Skip to content

Commit 1cc902f

Browse files
authored
Merge pull request #40 from Kasper24/typistack-improvements
refactor: typistack improvements
2 parents 303918b + cba2cdc commit 1cc902f

18 files changed

Lines changed: 740 additions & 361 deletions

File tree

packages/typiclient/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"dependencies": {
3131
"@repo/typiserver": "*",
3232
"superjson": "^2.2.2",
33-
"zod": "^3.22.4"
33+
"zod": "^3.25.6"
3434
},
3535
"devDependencies": {
3636
"@repo/eslint-config": "*",

packages/typiclient/src/client.ts

Lines changed: 134 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { deserialize } from "superjson";
2-
import type { RouteHandlerResponse, TypiRouter } from "@repo/typiserver";
2+
import type {
3+
RouteHandlerErrorDataResponse,
4+
RouteHandlerResponse,
5+
TypiRouter,
6+
} from "@repo/typiserver";
37
import {
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

261352
const isRouteHandlerResponse = (

packages/typiclient/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from "./client"
2-
export * from "./types"
1+
export * from "./client";
2+
export * from "./types";

0 commit comments

Comments
 (0)