export namespace Schemas { // export type AdminInitRequest = { password: string; setup_secret: string; username: string }; export type UpstreamBasicInfo = { created_at: string; id: string; name: string; protocol: string; updated_at: string; }; export type UpstreamTargetInfo = { created_at: string; enabled: boolean; id: string; is_backup: boolean; target_host: string; target_port: number; updated_at: string; upstream?: (null | UpstreamBasicInfo) | undefined; upstream_id: string; weight: number; }; export type CreateUpstreamRequestBody = { algorithm?: (string | null) | undefined; name: string; protocol: string; sticky_session?: (boolean | null) | undefined; upstream_targets: Array; }; export type CreateUpstreamTargetInfo = { enabled?: (boolean | null) | undefined; host: string; is_backup?: (boolean | null) | undefined; port: number; upstream_id: string; weight?: (number | null) | undefined; }; export type GetUpstreamParams = Partial<{ include_targets: boolean | null }>; export type GetUpstreamTargetsParams = Partial<{ include_upstream: boolean | null }>; export type HealthInfo = { errors?: (Array | null) | undefined; is_initialized: boolean; status: string; up_since: string; version: string; }; export type LoginRequest = { password: string; username: string }; export type PaginationInfo = { current_page: number; per_page: number; total_items: number; total_pages: number }; export type UpstreamTargetBasicInfo = { created_at: string; enabled: boolean; id: string; is_backup: boolean; target_host: string; target_port: number; updated_at: string; weight: number; }; export type UpdateUpstreamInfoResponse = { algorithm: string; created_at: string; created_by?: (string | null) | undefined; id: string; name: string; protocol: string; sticky_session: boolean; updated_at: string; upstream_targets: Array; }; export type UpdateUpstreamRequestBody = Partial<{ algorithm: string | null; name: string | null; protocol: string | null; sticky_session: boolean | null; upstream_targets: Array | null; }>; export type UpdateUpstreamTargetInfoResponse = { created_at: string; enabled: boolean; host: string; id: string; is_backup: boolean; port: number; updated_at: string; upstream_id: string; weight: number; }; export type UpdateUpstreamTargetRequestBody = Partial<{ enabled: boolean | null; host: string | null; is_backup: boolean | null; port: number | null; weight: number | null; }>; export type UpstreamInfoResponse = { algorithm: string; created_at: string; created_by?: (string | null) | undefined; id: string; name: string; protocol: string; sticky_session: boolean; updated_at: string; upstream_targets: Array; }; export type UpstreamListResponse = { items: Array; pagination: PaginationInfo }; export type UpstreamTargetBasicUpdateInfo = { enabled: boolean; id: number }; export type UpstreamTargetInfoResponse = { created_at: string; enabled: boolean; host: string; id: string; is_backup: boolean; port: number; updated_at: string; upstream_id: string; weight: number; }; export type UserInfo = { id: string; username: string }; // } export namespace Endpoints { // export type post_Init_admin = { method: "POST"; path: "/api/auth/init_admin"; requestFormat: "json"; parameters: { body: Schemas.AdminInitRequest; }; responses: { 200: unknown; 400: unknown; 401: unknown; 500: unknown }; }; export type post_Login = { method: "POST"; path: "/api/auth/login"; requestFormat: "json"; parameters: { body: Schemas.LoginRequest; }; responses: { 200: unknown; 401: unknown; 500: unknown }; }; export type get_Get_health_info = { method: "GET"; path: "/api/health/info"; requestFormat: "json"; parameters: never; responses: { 200: Schemas.HealthInfo; 404: unknown }; }; export type get_Get_upstream_target = { method: "GET"; path: "/api/nginx/upstream_targets/{upstream_target_id}"; requestFormat: "json"; parameters: { path: { upstream_target_id: string }; }; responses: { 200: Schemas.UpstreamTargetInfo; 404: unknown; 500: unknown }; }; export type delete_Remove_upstream_target = { method: "DELETE"; path: "/api/nginx/upstream_targets/{upstream_target_id}"; requestFormat: "json"; parameters: { path: { upstream_target_id: string }; }; responses: { 200: unknown; 401: unknown; 404: unknown; 500: unknown }; }; export type patch_Update_upstream_target = { method: "PATCH"; path: "/api/nginx/upstream_targets/{upstream_target_id}"; requestFormat: "json"; parameters: { path: { upstream_target_id: string }; body: Schemas.UpdateUpstreamTargetRequestBody; }; responses: { 200: Schemas.UpdateUpstreamTargetInfoResponse; 401: unknown; 404: unknown; 422: unknown; 500: unknown; }; }; export type get_Get_upstream_list = { method: "GET"; path: "/api/nginx/upstreams"; requestFormat: "json"; parameters: never; responses: { 200: Schemas.UpstreamListResponse; 500: unknown }; }; export type post_Create_upstream = { method: "POST"; path: "/api/nginx/upstreams"; requestFormat: "json"; parameters: { body: Schemas.CreateUpstreamRequestBody; }; responses: { 200: Schemas.UpstreamInfoResponse; 401: unknown; 422: unknown; 500: unknown }; }; export type get_Get_upstream = { method: "GET"; path: "/api/nginx/upstreams/{upstream_id}"; requestFormat: "json"; parameters: { path: { upstream_id: string }; }; responses: { 200: Schemas.UpstreamInfoResponse; 404: unknown; 500: unknown }; }; export type delete_Remove_upstream = { method: "DELETE"; path: "/api/nginx/upstreams/{upstream_id}"; requestFormat: "json"; parameters: { path: { upstream_id: string }; }; responses: { 200: unknown; 401: unknown; 404: unknown; 500: unknown }; }; export type patch_Update_upstream = { method: "PATCH"; path: "/api/nginx/upstreams/{upstream_id}"; requestFormat: "json"; parameters: { path: { upstream_id: string }; body: Schemas.UpdateUpstreamRequestBody; }; responses: { 200: Schemas.UpdateUpstreamInfoResponse; 401: unknown; 404: unknown; 422: unknown; 500: unknown }; }; export type post_Add_upstream_target = { method: "POST"; path: "/api/nginx/upstreams/{upstream_id}/targets"; requestFormat: "json"; parameters: { body: Schemas.CreateUpstreamTargetInfo; }; responses: { 200: Schemas.UpstreamTargetInfoResponse; 401: unknown; 422: unknown; 500: unknown }; }; export type get_Get_user_info = { method: "GET"; path: "/api/user/me"; requestFormat: "json"; parameters: never; responses: { 200: Schemas.UserInfo; 401: unknown; 500: unknown }; }; // } // export type EndpointByMethod = { post: { "/api/auth/init_admin": Endpoints.post_Init_admin; "/api/auth/login": Endpoints.post_Login; "/api/nginx/upstreams": Endpoints.post_Create_upstream; "/api/nginx/upstreams/{upstream_id}/targets": Endpoints.post_Add_upstream_target; }; get: { "/api/health/info": Endpoints.get_Get_health_info; "/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.get_Get_upstream_target; "/api/nginx/upstreams": Endpoints.get_Get_upstream_list; "/api/nginx/upstreams/{upstream_id}": Endpoints.get_Get_upstream; "/api/user/me": Endpoints.get_Get_user_info; }; delete: { "/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.delete_Remove_upstream_target; "/api/nginx/upstreams/{upstream_id}": Endpoints.delete_Remove_upstream; }; patch: { "/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.patch_Update_upstream_target; "/api/nginx/upstreams/{upstream_id}": Endpoints.patch_Update_upstream; }; }; // // export type PostEndpoints = EndpointByMethod["post"]; export type GetEndpoints = EndpointByMethod["get"]; export type DeleteEndpoints = EndpointByMethod["delete"]; export type PatchEndpoints = EndpointByMethod["patch"]; // // export type EndpointParameters = { body?: unknown; query?: Record; header?: Record; path?: Record; }; export type MutationMethod = "post" | "put" | "patch" | "delete"; export type Method = "get" | "head" | "options" | MutationMethod; type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text"; export type DefaultEndpoint = { parameters?: EndpointParameters | undefined; responses?: Record; responseHeaders?: Record; }; export type Endpoint = { operationId: string; method: Method; path: string; requestFormat: RequestFormat; parameters?: TConfig["parameters"]; meta: { alias: string; hasParameters: boolean; areParametersRequired: boolean; }; responses?: TConfig["responses"]; responseHeaders?: TConfig["responseHeaders"]; }; export interface Fetcher { decodePathParams?: (path: string, pathParams: Record) => string; encodeSearchParams?: (searchParams: Record | undefined) => URLSearchParams; // fetch: (input: { method: Method; url: URL; urlSearchParams?: URLSearchParams | undefined; parameters?: EndpointParameters | undefined; path: string; overrides?: RequestInit; throwOnStatusError?: boolean; }) => Promise; 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 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, ] 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 { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */ append: | (string & {})>( name: Name, 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, ) => (Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string) | null; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */ getSetCookie: () => string[]; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */ has: | (string & {})>(name: Name) => boolean; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */ set: | (string & {})>( name: Name, value: Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string, ) => void; forEach: ( callbackfn: ( value: TypedHeaderValues[keyof TypedHeaderValues] | (string & {}), key: Extract | (string & {}), parent: TypedHeaders, ) => void, thisArg?: any, ) => void; } /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */ export interface TypedSuccessResponse extends Omit { ok: true; status: TStatusCode; headers: never extends THeaders ? Headers : TypedHeaders; data: TSuccess; /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */ json: () => Promise; } /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */ export interface TypedErrorResponse extends Omit { ok: false; status: TStatusCode; headers: never extends THeaders ? Headers : TypedHeaders; data: TData; /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */ json: () => Promise; } export type TypedApiResponse = {}, THeaders = {}> = { [K in keyof TAllResponses]: K extends string ? K extends `${infer TStatusCode extends number}` ? TStatusCode extends SuccessStatusCode ? TypedSuccessResponse : TypedErrorResponse : never : K extends number ? K extends SuccessStatusCode ? TypedSuccessResponse : TypedErrorResponse : never; }[keyof TAllResponses]; export type SafeApiResponse = TEndpoint extends { responses: infer TResponses } ? TResponses extends Record ? TypedApiResponse : never : never; export type InferResponseByStatus = Extract< SafeApiResponse, { status: TStatusCode } >; type RequiredKeys = { [P in keyof T]-?: undefined extends T[P] ? never : P; }[keyof T]; type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T]; type NotNever = [T] extends [never] ? false : true; // // export class TypedStatusError extends Error { response: TypedErrorResponse; status: number; constructor(response: TypedErrorResponse) { super(`HTTP ${response.status}: ${response.statusText}`); this.name = "TypedStatusError"; this.response = response; this.status = response.status; } } // // export class ApiClient { baseUrl: string = ""; successStatusCodes = successStatusCodes; errorStatusCodes = errorStatusCodes; constructor(public fetcher: Fetcher) {} setBaseUrl(baseUrl: string) { this.baseUrl = baseUrl; return this; } /** * Replace path parameters in URL * 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}`); }; /** Uses URLSearchParams, skips null/undefined values */ defaultEncodeSearchParams = (queryParams: Record | undefined): URLSearchParams | undefined => { if (!queryParams) return; const searchParams = new URLSearchParams(); Object.entries(queryParams).forEach(([key, value]) => { if (value != null) { // Skip null/undefined values if (Array.isArray(value)) { value.forEach((val) => val != null && searchParams.append(key, String(val))); } else { searchParams.append(key, String(value)); } } }); return searchParams; }; defaultParseResponseData = async (response: Response): Promise => { const contentType = response.headers.get("content-type") ?? ""; if (contentType.startsWith("text/")) { return await response.text(); } if (contentType === "application/octet-stream") { return await response.arrayBuffer(); } if ( contentType.includes("application/json") || (contentType.includes("application/") && contentType.includes("json")) || contentType === "*/*" ) { try { return await response.json(); } catch { return undefined; } } return; }; // post( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } > ): Promise, { data: {} }>["data"]>; post( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } > ): Promise>; post( path: Path, ...params: MaybeOptionalArg ): Promise { return this.request("post", path, ...params); } // // get( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } > ): Promise, { data: {} }>["data"]>; get( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } > ): Promise>; get( path: Path, ...params: MaybeOptionalArg ): Promise { return this.request("get", path, ...params); } // // delete( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } > ): Promise, { data: {} }>["data"]>; delete( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } > ): Promise>; delete( path: Path, ...params: MaybeOptionalArg ): Promise { return this.request("delete", path, ...params); } // // patch( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } > ): Promise, { data: {} }>["data"]>; patch( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } > ): Promise>; patch( path: Path, ...params: MaybeOptionalArg ): Promise { return this.request("patch", path, ...params); } // // /** * Generic request method with full type-safety for any endpoint */ request< TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath], >( method: TMethod, path: TPath, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } > ): Promise, { data: {} }>["data"]>; request< TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath], >( method: TMethod, path: TPath, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } > ): 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 parametersToSend: EndpointParameters = {}; if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body; if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query; if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header; if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path; const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)( this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record, ); const url = new URL(resolvedPath); const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query); const promise = this.fetcher .fetch({ method: method, path: path as string, url, urlSearchParams, parameters: Object.keys(fetchParams).length ? fetchParams : undefined, overrides, throwOnStatusError, }) .then(async (response) => { const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response); const typedResponse = Object.assign(response, { data: data, json: () => Promise.resolve(data), }) as SafeApiResponse; if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) { throw new TypedStatusError(typedResponse as never); } return withResponse ? typedResponse : data; }); return promise as Extract, { data: {} }>["data"]; } // } export function createApiClient(fetcher: Fetcher, baseUrl?: string) { return new ApiClient(fetcher).setBaseUrl(baseUrl ?? ""); } /** Example usage: const api = createApiClient((method, url, params) => fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()), ); api.get("/users").then((users) => console.log(users)); api.post("/users", { body: { name: "John" } }).then((user) => console.log(user)); api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user)); // With error handling const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true }); if (result.ok) { // Access data directly const user = result.data; console.log(user); // Or use the json() method for compatibility const userFromJson = await result.json(); console.log(userFromJson); } else { const error = result.data; console.error(`Error ${result.status}:`, error); } */ //