@@ -13,7 +13,6 @@ import type {
13
13
const DEFAULT_HEADERS = {
14
14
"Content-Type" : "application/json" ,
15
15
} ;
16
- const TRAILING_SLASH_RE = / \/ * $ / ;
17
16
18
17
// Note: though "any" is considered bad practice in general, this library relies
19
18
// on "any" for type inference only it can give. Same goes for the "{}" type.
@@ -32,28 +31,46 @@ interface ClientOptions extends Omit<RequestInit, "headers"> {
32
31
// headers override to make typing friendlier
33
32
headers ?: HeadersOptions ;
34
33
}
34
+
35
35
export type HeadersOptions =
36
36
| HeadersInit
37
37
| Record < string , string | number | boolean | null | undefined > ;
38
+
38
39
export type QuerySerializer < T > = (
39
40
query : T extends { parameters : any }
40
41
? NonNullable < T [ "parameters" ] [ "query" ] >
41
42
: Record < string , unknown > ,
42
43
) => string ;
44
+
43
45
export type BodySerializer < T > = ( body : OperationRequestBodyContent < T > ) => any ;
46
+
44
47
export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "stream" ;
48
+
45
49
export interface DefaultParamsOption {
46
50
params ?: { query ?: Record < string , unknown > } ;
47
51
}
52
+
53
+ export interface EmptyParameters {
54
+ query ?: never ;
55
+ header ?: never ;
56
+ path ?: never ;
57
+ cookie ?: never ;
58
+ }
59
+
48
60
export type ParamsOption < T > = T extends { parameters : any }
49
- ? { params : NonNullable < T [ "parameters" ] > }
61
+ ? T [ "parameters" ] extends EmptyParameters
62
+ ? DefaultParamsOption
63
+ : { params : NonNullable < T [ "parameters" ] > }
50
64
: DefaultParamsOption ;
65
+
51
66
export type RequestBodyOption < T > = OperationRequestBodyContent < T > extends never
52
67
? { body ?: never }
53
68
: undefined extends OperationRequestBodyContent < T >
54
69
? { body ?: OperationRequestBodyContent < T > }
55
70
: { body : OperationRequestBodyContent < T > } ;
71
+
56
72
export type FetchOptions < T > = RequestOptions < T > & Omit < RequestInit , "body" > ;
73
+
57
74
export type FetchResponse < T > =
58
75
| {
59
76
data : FilterKeys < SuccessResponse < ResponseObjectMap < T > > , MediaType > ;
@@ -65,6 +82,7 @@ export type FetchResponse<T> =
65
82
error : FilterKeys < ErrorResponse < ResponseObjectMap < T > > , MediaType > ;
66
83
response : Response ;
67
84
} ;
85
+
68
86
export type RequestOptions < T > = ParamsOption < T > &
69
87
RequestBodyOption < T > & {
70
88
querySerializer ?: QuerySerializer < T > ;
@@ -81,6 +99,10 @@ export default function createClient<Paths extends {}>(
81
99
bodySerializer : globalBodySerializer ,
82
100
...options
83
101
} = clientOptions ;
102
+ let baseUrl = options . baseUrl ?? "" ;
103
+ if ( baseUrl . endsWith ( "/" ) ) {
104
+ baseUrl = baseUrl . slice ( 0 , - 1 ) ;
105
+ }
84
106
85
107
async function coreFetch < P extends keyof Paths , M extends HttpMethod > (
86
108
url : P ,
@@ -98,7 +120,7 @@ export default function createClient<Paths extends {}>(
98
120
99
121
// URL
100
122
const finalURL = createFinalURL ( url as string , {
101
- baseUrl : options . baseUrl ,
123
+ baseUrl,
102
124
params,
103
125
querySerializer,
104
126
} ) ;
@@ -159,13 +181,20 @@ export default function createClient<Paths extends {}>(
159
181
return { error, response : response as any } ;
160
182
}
161
183
184
+ type GetPaths = PathsWithMethod < Paths , "get" > ;
185
+ type GetFetchOptions < P extends GetPaths > = FetchOptions <
186
+ FilterKeys < Paths [ P ] , "get" >
187
+ > ;
188
+
162
189
return {
163
190
/** Call a GET endpoint */
164
- async GET < P extends PathsWithMethod < Paths , "get" > > (
191
+ async GET < P extends GetPaths > (
165
192
url : P ,
166
- init : FetchOptions < FilterKeys < Paths [ P ] , "get" > > ,
193
+ ...init : GetFetchOptions < P > extends DefaultParamsOption // little hack to allow the 2nd param to be omitted if nothing is required (only for GET)
194
+ ? [ GetFetchOptions < P > ?]
195
+ : [ GetFetchOptions < P > ]
167
196
) {
168
- return coreFetch < P , "get" > ( url , { ...init , method : "GET" } as any ) ;
197
+ return coreFetch < P , "get" > ( url , { ...init [ 0 ] , method : "GET" } as any ) ;
169
198
} ,
170
199
/** Call a PUT endpoint */
171
200
async PUT < P extends PathsWithMethod < Paths , "put" > > (
@@ -245,26 +274,20 @@ export function defaultBodySerializer<T>(body: T): string {
245
274
246
275
/** Construct URL string from baseUrl and handle path and query params */
247
276
export function createFinalURL < O > (
248
- url : string ,
277
+ pathname : string ,
249
278
options : {
250
- baseUrl ? : string ;
279
+ baseUrl : string ;
251
280
params : { query ?: Record < string , unknown > ; path ?: Record < string , unknown > } ;
252
281
querySerializer : QuerySerializer < O > ;
253
282
} ,
254
283
) : string {
255
- let finalURL = `${
256
- options . baseUrl ? options . baseUrl . replace ( TRAILING_SLASH_RE , "" ) : ""
257
- } ${ url as string } `;
258
- if ( options . params . path ) {
259
- for ( const [ k , v ] of Object . entries ( options . params . path ) ) {
260
- finalURL = finalURL . replace ( `{${ k } }` , encodeURIComponent ( String ( v ) ) ) ;
261
- }
284
+ let finalURL = `${ options . baseUrl } ${ pathname } ` ;
285
+ for ( const [ k , v ] of Object . entries ( options . params . path ?? { } ) ) {
286
+ finalURL = finalURL . replace ( `{${ k } }` , encodeURIComponent ( String ( v ) ) ) ;
262
287
}
263
- if ( options . params . query ) {
264
- const search = options . querySerializer ( options . params . query as any ) ;
265
- if ( search ) {
266
- finalURL += `?${ search } ` ;
267
- }
288
+ const search = options . querySerializer ( ( options . params . query as any ) ?? { } ) ;
289
+ if ( search ) {
290
+ finalURL += `?${ search } ` ;
268
291
}
269
292
return finalURL ;
270
293
}
0 commit comments