import type { ApiError } from "@sprint/shared"; import { apiContract } from "@sprint/shared"; import type { AppRoute, AppRouter } from "@ts-rest/core"; import { checkZodSchema, initClient, isAppRoute } from "@ts-rest/core"; import { getCsrfToken, getServerURL } from "@/lib/utils"; type ApiResult = { data: T | null; error: string | null; status: number; }; const rawClient = initClient(apiContract, { baseUrl: getServerURL(), baseHeaders: { "X-CSRF-Token": () => getCsrfToken() || "", }, credentials: "include", validateResponse: true, throwOnUnknownStatus: false, }); function toErrorMessage(error: unknown): string { if (typeof error === "string") return error; if (error instanceof Error) return error.message; if (error && typeof error === "object") { const maybeApiError = error as ApiError; if (maybeApiError.details) { const messages = Object.values(maybeApiError.details).flat(); if (messages.length > 0) return messages.join(", "); } if (typeof maybeApiError.error === "string") return maybeApiError.error; } return "unexpected error"; } function validateRequest(route: AppRoute, input?: { body?: unknown; query?: unknown }): string | null { if (!input) return null; if ("body" in route && route.body && "body" in input) { const result = checkZodSchema(input.body, route.body); if (!result.success) { return result.error.issues.map((issue) => issue.message).join(", ") || "invalid request body"; } } if ("query" in route && route.query && "query" in input) { const result = checkZodSchema(input.query, route.query); if (!result.success) { return result.error.issues.map((issue) => issue.message).join(", ") || "invalid query params"; } } return null; } async function requestResult( responsePromise: Promise, ): Promise> { try { const response = await responsePromise; if (response.status >= 200 && response.status < 300) { return { data: response.body, error: null, status: response.status }; } return { data: null, error: toErrorMessage(response.body), status: response.status }; } catch (error) { return { data: null, error: toErrorMessage(error), status: 0 }; } } type WrappedClient = { [K in keyof T]: T[K] extends (...args: infer A) => Promise ? (...args: A) => Promise> : T[K] extends object ? WrappedClient : T[K]; }; function wrapClient(router: TRouter, client: unknown): unknown { const entries = Object.entries(router).map(([key, route]) => { const value = (client as Record)[key]; if (isAppRoute(route) && typeof value === "function") { return [ key, async (input?: { body?: unknown; query?: unknown; headers?: Record }) => { const validationError = validateRequest(route, input); if (validationError) { return { data: null, error: validationError, status: 0 } as ApiResult; } return requestResult( (value as (args?: unknown) => Promise<{ status: number; body: unknown }>)(input), ); }, ]; } if (route && typeof route === "object" && value && typeof value === "object") { return [key, wrapClient(route as AppRouter, value)]; } return [key, value]; }); return Object.fromEntries(entries); } export const apiClient = wrapClient(apiContract, rawClient) as WrappedClient; export type { ApiResult };