11import { deserialize } from "superjson" ;
2- import type { RouteHandlerResponse , TypiRouter } from "@repo/typiserver" ;
2+ import type {
3+ RouteHandlerErrorDataResponse ,
4+ RouteHandlerResponse ,
5+ TypiRouter ,
6+ } from "@repo/typiserver" ;
37import {
48 type HttpMethod ,
59 type HttpStatusCode ,
@@ -44,11 +48,11 @@ export class TypiClient {
4448 ) ;
4549 }
4650 } ,
47- apply : ( _ , __ , [ input ] ) => {
51+ apply : ( _ , __ , [ args ] ) => {
4852 const method = path [ path . length - 1 ] . toUpperCase ( ) as HttpMethod ;
4953 const urlWithoutMethod = this . path . slice ( 0 , - 1 ) . join ( "/" ) ;
5054 const url = `${ this . baseUrl } /${ urlWithoutMethod } ` ;
51- return this . executeRequest ( url , method , input ) ;
55+ return this . executeRequest ( url , method , args ?. input , args ?. options ) ;
5256 } ,
5357 } ) as any ;
5458 }
@@ -79,11 +83,11 @@ export class TypiClient {
7983 . join ( "; " ) ;
8084 }
8185
82- private async buildHeaders ( input : any ) {
86+ private async buildHeaders ( input : any , hasBody : boolean , hasFiles : boolean ) {
8387 const cookieHeader = this . buildCookieHeader ( input ?. cookies ) ;
8488
8589 let headers = {
86- "Content-Type" : "application/json" ,
90+ ... ( hasBody && ! hasFiles ? { "Content-Type" : "application/json" } : { } ) ,
8791 ...( input ?. headers || { } ) ,
8892 ...( cookieHeader ? { Cookie : cookieHeader } : { } ) ,
8993 } ;
@@ -109,29 +113,86 @@ export class TypiClient {
109113 return headers ;
110114 }
111115
112- private async buildRequestConfig ( method : HttpMethod , input : any ) {
113- const headers = await this . buildHeaders ( input ) ;
116+ private hasFileData ( obj : any ) : boolean {
117+ if ( obj instanceof File || obj instanceof Blob ) {
118+ return true ;
119+ }
120+ if ( obj && typeof obj === "object" ) {
121+ return Object . values ( obj ) . some ( ( value ) => this . hasFileData ( value ) ) ;
122+ }
123+ return false ;
124+ }
125+
126+ private buildFormData ( data : any ) : FormData {
127+ const formData = new FormData ( ) ;
128+
129+ const appendToFormData = ( obj : any , prefix = "" ) => {
130+ for ( const key in obj ) {
131+ if ( obj . hasOwnProperty ( key ) ) {
132+ const value = obj [ key ] ;
133+ const formKey = prefix ? `${ prefix } [${ key } ]` : key ;
134+
135+ if ( value instanceof File || value instanceof Blob ) {
136+ formData . append ( formKey , value ) ;
137+ } else if (
138+ value &&
139+ typeof value === "object" &&
140+ ! ( value instanceof Date )
141+ ) {
142+ appendToFormData ( value , formKey ) ;
143+ } else if ( value !== null && value !== undefined ) {
144+ formData . append ( formKey , String ( value ) ) ;
145+ }
146+ }
147+ }
148+ } ;
149+
150+ appendToFormData ( data ) ;
151+ return formData ;
152+ }
153+
154+ private async buildRequestConfig (
155+ method : HttpMethod ,
156+ input : any ,
157+ options ?: ClientOptions
158+ ) {
159+ const hasBody = method !== "get" && method !== "head" && input ?. body ;
160+ const hasFiles = input ?. body && this . hasFileData ( input . body ) ;
161+ const headers = await this . buildHeaders ( input , hasBody , hasFiles ) ;
162+
163+ let body : string | FormData | undefined ;
164+
165+ if ( hasBody ) {
166+ if ( hasFiles ) body = this . buildFormData ( input . body ) ;
167+ else body = JSON . stringify ( input . body ) ;
168+ }
114169
115170 return {
116- credentials : this . options ?. credentials ,
171+ credentials : options ?. credentials || this . options ?. credentials ,
117172 method : method ,
118173 headers : headers ,
119- body :
120- method !== "get" && method !== "head" && input ?. body
121- ? JSON . stringify ( input . body )
174+ body : body ,
175+ signal : options ?. timeout
176+ ? AbortSignal . timeout ( options . timeout )
177+ : this . options ?. timeout
178+ ? AbortSignal . timeout ( this . options . timeout )
122179 : undefined ,
123- signal : this . options ?. timeout
124- ? AbortSignal . timeout ( this . options . timeout )
125- : undefined ,
126180 } as RequestInit ;
127181 }
128182
129- private async executeInterceptors (
130- url : URL ,
131- config : RequestInit ,
132- response ?: Response ,
133- error ?: any
134- ) {
183+ private async executeInterceptors ( {
184+ url,
185+ config,
186+ response,
187+ error,
188+ options,
189+ } : {
190+ url : URL ;
191+ config : RequestInit ;
192+ response ?: Response ;
193+ error ?: any ;
194+ options ?: ClientOptions ;
195+ } ) {
135196 // Handle request interceptors
136197 if ( ! response && ! error ) {
137198 for ( const requestInterceptor of this . interceptors ?. onRequest || [ ] ) {
@@ -152,7 +213,7 @@ export class TypiClient {
152213 path : url . pathname ,
153214 config,
154215 response,
155- retry : ( ) => this . makeRequest ( url , config ) ,
216+ retry : ( ) => this . makeRequest ( url , config , options ) ,
156217 } ) ;
157218 if ( isRouteHandlerResponse ( result ) ) {
158219 return { result } ;
@@ -167,7 +228,7 @@ export class TypiClient {
167228 path : url . pathname ,
168229 config,
169230 error,
170- retry : ( ) => this . makeRequest ( url , config ) ,
231+ retry : ( ) => this . makeRequest ( url , config , options ) ,
171232 } ) ;
172233 if ( isRouteHandlerResponse ( result ) ) {
173234 return { result } ;
@@ -178,53 +239,80 @@ export class TypiClient {
178239 return { } ;
179240 }
180241
181- private async makeRequest ( url : URL , config : RequestInit ) : Promise < any > {
242+ private async makeRequest (
243+ url : URL ,
244+ config : RequestInit ,
245+ options ?: ClientOptions
246+ ) : Promise < any > {
182247 try {
183248 const response = await fetch ( url . toString ( ) , config ) ;
184249
185- const interceptorResult = await this . executeInterceptors (
250+ const interceptorResult = await this . executeInterceptors ( {
186251 url,
187252 config,
188- response
189- ) ;
253+ response,
254+ options,
255+ } ) ;
190256 if ( interceptorResult . result ) {
191257 return interceptorResult . result ;
192258 }
193259
194260 const data = deserialize ( await response . json ( ) ) ;
195261 const status = getStatus ( response . status as HttpStatusCode ) . key ;
196262
197- console [ status === "OK" ? "log" : "error" ] (
198- `Request to ${ url . toString ( ) } returned status ${ status } ` ,
199- data
200- ) ;
263+ console [ status === "OK" ? "log" : "warn" ] ( {
264+ URL : url . toString ( ) ,
265+ config : config ,
266+ status : status ,
267+ data : data ,
268+ } ) ;
269+
270+ if (
271+ status !== "OK" &&
272+ ( options ?. throwOnErrorStatus ?? this . options ?. throwOnErrorStatus )
273+ ) {
274+ throw new Error ( ( data as RouteHandlerErrorDataResponse ) . error . message ) ;
275+ }
201276
202277 return {
203278 status : status ,
204279 data : data as any ,
205280 response : response ,
206281 } ;
207282 } catch ( error ) {
208- console . error ( `Error making request to ${ url . toString ( ) } ` , error ) ;
209- const interceptorResult = await this . executeInterceptors (
283+ console . error ( {
284+ URL : url . toString ( ) ,
285+ config : config ,
286+ error : error instanceof Error ? error : undefined ,
287+ } ) ;
288+ const interceptorResult = await this . executeInterceptors ( {
210289 url,
211290 config,
212- undefined ,
213- error
214- ) ;
291+ error ,
292+ options ,
293+ } ) ;
215294 if ( interceptorResult . result ) {
216295 return interceptorResult . result ;
217296 }
218297 throw error ;
219298 }
220299 }
221300
222- private async executeRequest ( path : string , method : HttpMethod , input : any ) {
301+ private async executeRequest (
302+ path : string ,
303+ method : HttpMethod ,
304+ input : any ,
305+ options ?: ClientOptions
306+ ) {
223307 const url = this . buildUrl ( path , input ) ;
224- let config = await this . buildRequestConfig ( method , input ) ;
308+ let config = await this . buildRequestConfig ( method , input , options ) ;
225309
226310 // Execute request interceptors
227- const interceptorResult = await this . executeInterceptors ( url , config ) ;
311+ const interceptorResult = await this . executeInterceptors ( {
312+ url,
313+ config,
314+ options,
315+ } ) ;
228316
229317 if ( interceptorResult . result ) {
230318 return interceptorResult . result ;
@@ -234,11 +322,14 @@ export class TypiClient {
234322 config = interceptorResult . config ;
235323 }
236324
237- return this . makeRequest ( url , config ) ;
325+ return this . makeRequest ( url , config , options ) ;
238326 }
239327}
240328
241- export function createTypiClient < T extends TypiRouter > ( {
329+ export function createTypiClient <
330+ TRouter extends TypiRouter ,
331+ TOptions extends ClientOptions ,
332+ > ( {
242333 baseUrl,
243334 baseHeaders,
244335 interceptors,
@@ -247,15 +338,15 @@ export function createTypiClient<T extends TypiRouter>({
247338 baseUrl : string ;
248339 baseHeaders ?: BaseHeaders ;
249340 interceptors ?: RequestInterceptors ;
250- options ?: ClientOptions ;
251- } ) : TypiClientInstance < T > {
341+ options ?: TOptions ;
342+ } ) : TypiClientInstance < TRouter , TOptions > {
252343 return new TypiClient (
253344 baseUrl ,
254345 [ ] ,
255346 baseHeaders ,
256347 interceptors ,
257348 options
258- ) as TypiClientInstance < T > ;
349+ ) as TypiClientInstance < TRouter , TOptions > ;
259350}
260351
261352const isRouteHandlerResponse = (
0 commit comments