diff --git a/apps/frontend/app/generated/api-client.ts b/apps/frontend/app/generated/api-client/api-client.ts similarity index 80% rename from apps/frontend/app/generated/api-client.ts rename to apps/frontend/app/generated/api-client/api-client.ts index 67c0787..8a5e6b3 100644 --- a/apps/frontend/app/generated/api-client.ts +++ b/apps/frontend/app/generated/api-client/api-client.ts @@ -14,9 +14,9 @@ export namespace Endpoints { // export type get_Get_health_info = { - method: 'GET'; - path: '/api/health/info'; - requestFormat: 'json'; + method: "GET"; + path: "/api/health/info"; + requestFormat: "json"; parameters: never; responses: { 200: Schemas.HealthInfo; 404: unknown }; }; @@ -27,14 +27,14 @@ export namespace Endpoints { // export type EndpointByMethod = { get: { - '/api/health/info': Endpoints.get_Get_health_info; + "/api/health/info": Endpoints.get_Get_health_info; }; }; // // -export type GetEndpoints = EndpointByMethod['get']; +export type GetEndpoints = EndpointByMethod["get"]; // // @@ -45,10 +45,10 @@ export type EndpointParameters = { path?: Record; }; -export type MutationMethod = 'post' | 'put' | 'patch' | 'delete'; -export type Method = 'get' | 'head' | 'options' | MutationMethod; +export type MutationMethod = "post" | "put" | "patch" | "delete"; +export type Method = "get" | "head" | "options" | MutationMethod; -type RequestFormat = 'json' | 'form-data' | 'form-url' | 'binary' | 'text'; +type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text"; export type DefaultEndpoint = { parameters?: EndpointParameters | undefined; @@ -61,14 +61,14 @@ export type Endpoint = { method: Method; path: string; requestFormat: RequestFormat; - parameters?: TConfig['parameters']; + parameters?: TConfig["parameters"]; meta: { alias: string; hasParameters: boolean; areParametersRequired: boolean; }; - responses?: TConfig['responses']; - responseHeaders?: TConfig['responseHeaders']; + responses?: TConfig["responses"]; + responseHeaders?: TConfig["responseHeaders"]; }; export interface Fetcher { @@ -87,28 +87,30 @@ export interface Fetcher { parseResponseData?: (response: Response) => Promise; } -export const successStatusCodes = [200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308] as const; +export const successStatusCodes = [ + 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308, +] as const; export type SuccessStatusCode = (typeof successStatusCodes)[number]; export const errorStatusCodes = [ - 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, - 504, 505, 506, 507, 508, 510, 511, + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, + 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, ] as const; export type ErrorStatusCode = (typeof errorStatusCodes)[number]; // Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3 export interface TypedHeaders | unknown> - extends Omit { + extends Omit { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */ append: | (string & {})>( name: Name, - value: Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string + value: Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string, ) => void; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */ delete: | (string & {})>(name: Name) => void; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */ get: | (string & {})>( - name: Name + name: Name, ) => (Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string) | null; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */ getSetCookie: () => string[]; @@ -117,20 +119,21 @@ export interface TypedHeaders | /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */ set: | (string & {})>( name: Name, - value: Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string + value: Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string, ) => void; forEach: ( callbackfn: ( value: TypedHeaderValues[keyof TypedHeaderValues] | (string & {}), key: Extract | (string & {}), - parent: TypedHeaders + parent: TypedHeaders, ) => void, - thisArg?: any + thisArg?: any, ) => void; } /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */ -export interface TypedSuccessResponse extends Omit { +export interface TypedSuccessResponse + extends Omit { ok: true; status: TStatusCode; headers: never extends THeaders ? Headers : TypedHeaders; @@ -140,7 +143,8 @@ export interface TypedSuccessResponse extends O } /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */ -export interface TypedErrorResponse extends Omit { +export interface TypedErrorResponse + extends Omit { ok: false; status: TStatusCode; headers: never extends THeaders ? Headers : TypedHeaders; @@ -157,10 +161,10 @@ export type TypedApiResponse : never : K extends number - ? K extends SuccessStatusCode - ? TypedSuccessResponse - : TypedErrorResponse - : never; + ? K extends SuccessStatusCode + ? TypedSuccessResponse + : TypedErrorResponse + : never; }[keyof TAllResponses]; export type SafeApiResponse = TEndpoint extends { responses: infer TResponses } @@ -169,7 +173,10 @@ export type SafeApiResponse = TEndpoint extends { responses: infer TR : never : never; -export type InferResponseByStatus = Extract, { status: TStatusCode }>; +export type InferResponseByStatus = Extract< + SafeApiResponse, + { status: TStatusCode } +>; type RequiredKeys = { [P in keyof T]-?: undefined extends T[P] ? never : P; @@ -186,7 +193,7 @@ export class TypedStatusError extends Error { status: number; constructor(response: TypedErrorResponse) { super(`HTTP ${response.status}: ${response.statusText}`); - this.name = 'TypedStatusError'; + this.name = "TypedStatusError"; this.response = response; this.status = response.status; } @@ -195,7 +202,7 @@ export class TypedStatusError extends Error { // export class ApiClient { - baseUrl: string = ''; + baseUrl: string = ""; successStatusCodes = successStatusCodes; errorStatusCodes = errorStatusCodes; @@ -211,7 +218,9 @@ export class ApiClient { * Supports both OpenAPI format {param} and Express format :param */ defaultDecodePathParams = (url: string, params: Record): string => { - return url.replace(/{(\w+)}/g, (_, key: string) => params[key] || `{${key}}`).replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || `:${key}`); + return url + .replace(/{(\w+)}/g, (_, key: string) => params[key] || `{${key}}`) + .replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || `:${key}`); }; /** Uses URLSearchParams, skips null/undefined values */ @@ -234,16 +243,20 @@ export class ApiClient { }; defaultParseResponseData = async (response: Response): Promise => { - const contentType = response.headers.get('content-type') ?? ''; - if (contentType.startsWith('text/')) { + const contentType = response.headers.get("content-type") ?? ""; + if (contentType.startsWith("text/")) { return await response.text(); } - if (contentType === 'application/octet-stream') { + if (contentType === "application/octet-stream") { return await response.arrayBuffer(); } - if (contentType.includes('application/json') || (contentType.includes('application/') && contentType.includes('json')) || contentType === '*/*') { + if ( + contentType.includes("application/json") || + (contentType.includes("application/") && contentType.includes("json")) || + contentType === "*/*" + ) { try { return await response.json(); } catch { @@ -264,7 +277,7 @@ export class ApiClient { : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } > - ): Promise, { data: {} }>['data']>; + ): Promise, { data: {} }>["data"]>; get( path: Path, @@ -277,8 +290,11 @@ export class ApiClient { > ): Promise>; - get(path: Path, ...params: MaybeOptionalArg): Promise { - return this.request('get', path, ...params); + get( + path: Path, + ...params: MaybeOptionalArg + ): Promise { + return this.request("get", path, ...params); } // @@ -286,7 +302,11 @@ export class ApiClient { /** * Generic request method with full type-safety for any endpoint */ - request( + request< + TMethod extends keyof EndpointByMethod, + TPath extends keyof EndpointByMethod[TMethod], + TEndpoint extends EndpointByMethod[TMethod][TPath], + >( method: TMethod, path: TPath, ...params: MaybeOptionalArg< @@ -296,9 +316,13 @@ export class ApiClient { : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } > - ): Promise, { data: {} }>['data']>; + ): Promise, { data: {} }>["data"]>; - request( + request< + TMethod extends keyof EndpointByMethod, + TPath extends keyof EndpointByMethod[TMethod], + TEndpoint extends EndpointByMethod[TMethod][TPath], + >( method: TMethod, path: TPath, ...params: MaybeOptionalArg< @@ -310,14 +334,19 @@ export class ApiClient { > ): Promise>; - request( - method: TMethod, - path: TPath, - ...params: MaybeOptionalArg - ): Promise { + request< + TMethod extends keyof EndpointByMethod, + TPath extends keyof EndpointByMethod[TMethod], + TEndpoint extends EndpointByMethod[TMethod][TPath], + >(method: TMethod, path: TPath, ...params: MaybeOptionalArg): Promise { const requestParams = params[0]; const withResponse = requestParams?.withResponse; - const { withResponse: _, throwOnStatusError = withResponse ? false : true, overrides, ...fetchParams } = requestParams || {}; + const { + withResponse: _, + throwOnStatusError = withResponse ? false : true, + overrides, + ...fetchParams + } = requestParams || {}; const parametersToSend: EndpointParameters = {}; if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body; @@ -327,9 +356,8 @@ export class ApiClient { const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)( this.baseUrl + (path as string), - (parametersToSend.path ?? {}) as Record + (parametersToSend.path ?? {}) as Record, ); - console.log('Resolved Path:', resolvedPath); const url = new URL(resolvedPath); const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query); @@ -357,13 +385,13 @@ export class ApiClient { return withResponse ? typedResponse : data; }); - return promise as Extract, { data: {} }>['data']; + return promise as Extract, { data: {} }>["data"]; } // } export function createApiClient(fetcher: Fetcher, baseUrl?: string) { - return new ApiClient(fetcher).setBaseUrl(baseUrl ?? ''); + return new ApiClient(fetcher).setBaseUrl(baseUrl ?? ""); } /** diff --git a/apps/frontend/app/generated/tanstack-client.ts b/apps/frontend/app/generated/api-client/tanstack-client.ts similarity index 100% rename from apps/frontend/app/generated/tanstack-client.ts rename to apps/frontend/app/generated/api-client/tanstack-client.ts diff --git a/apps/frontend/app/lib/api.ts b/apps/frontend/app/lib/api.ts index 0c54e4f..d201d9a 100644 --- a/apps/frontend/app/lib/api.ts +++ b/apps/frontend/app/lib/api.ts @@ -1,5 +1,5 @@ import type { AxiosInstance, AxiosResponse } from 'axios'; -import { type Fetcher, type Method, createApiClient } from '../generated/api-client'; +import { type Fetcher, type Method, createApiClient } from '../generated/api-client/api-client'; import { TanstackQueryApiClient } from '../generated/tanstack-client'; const API_BASE_URL: string | undefined = import.meta.env.VITE_API_BASE_URL; diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 3f85316..9f9b8a4 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -8,7 +8,7 @@ "start": "react-router-serve ./build/server/index.js", "typecheck": "react-router typegen && tsc", "test": "echo \"No tests specified\" && exit 0", - "generate:openapi": "typed-openapi ../api/swagger.json --tanstack tanstack-client.ts -o ./app/generated/api-client.ts" + "generate:openapi": "typed-openapi ../api/swagger.json --tanstack tanstack-client.ts -o ./app/generated/api-client/api-client.ts" }, "dependencies": { "@radix-ui/themes": "^3.2.1",