/* eslint-disable @typescript-eslint/no-empty-object-type */ import z, { ZodObject, ZodType } from "zod"; import type { RelayAdapter, RelayInput, RelayResponse } from "./adapter.ts"; import { Route, type Routes } from "./route.ts"; /** * Factory method for generating a new relay client instance. * * @param config - Client configuration. * @param procedures - Map of routes to make available to the client. */ export function makeClient(config: Config, routes: TRoutes): RelayClient { const client: any = { getUrl: config.adapter.getUrl.bind(config.adapter), request: config.adapter.request.bind(config.adapter), }; for (const key in routes) { const route = routes[key]; if (route instanceof Route) { client[key] = getRouteFn(route, config); } else { client[key] = getNestedRoute(config, route); } } return client; } /* |-------------------------------------------------------------------------------- | Helpers |-------------------------------------------------------------------------------- */ function getNestedRoute(config: Config, routes: TRoutes): RelayClient { const nested: any = {}; for (const key in routes) { const route = routes[key]; if (route instanceof Route) { nested[key] = getRouteFn(route, config); } else { nested[key] = getNestedRoute(config, route); } } return nested; } function getRouteFn(route: Route, { adapter }: Config) { return async (options: any = {}) => { const input: RelayInput = { method: route.state.method, endpoint: route.state.path, query: "", }; // ### Params // Prepare request parameters by replacing :param notations with the // parameter argument provided. if (route.state.params !== undefined) { const params = await toParsedArgs( route.state.params, options.params, `Invalid 'params' passed to ${route.state.path} handler.`, ); for (const key in params) { input.endpoint = input.endpoint.replace(`:${key}`, encodeURIComponent(params[key])); } } // ### Query // Prepare request query by looping through the query argument and // creating a query string to pass onto the fetch request. if (route.state.query !== undefined) { const query = await toParsedArgs( route.state.query, options.query, `Invalid 'query' passed to ${route.state.path} handler.`, ); const pieces: string[] = []; for (const key in query) { pieces.push(`${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`); } if (pieces.length > 0) { input.query = `?${pieces.join("&")}`; } } // ### Body // Attach the body to the input which is handled internally based on the // type of fetch body is submitted. if (route.state.body !== undefined) { input.body = await toParsedArgs( route.state.body, options.body, `Invalid 'body' passed to ${route.state.path} handler.`, ); } // ### Request Init // List of request init options that we can extract and forward to the // request adapter. if (options.headers !== undefined) { input.headers = new Headers(options.headers); } // ### Fetch const response = route.state.content === "json" ? await adapter.json(input) : await adapter.data(input); if ("data" in response && route.state.output !== undefined) { response.data = route.state.output.parse(response.data); } return response; }; } async function toParsedArgs( zod: ZodType, args: unknown, msg: string, ): Promise> { const result = await zod.safeParseAsync(args); if (result.success === false) { throw new Error(msg); } return result.data as Record; } /* |-------------------------------------------------------------------------------- | Types |-------------------------------------------------------------------------------- */ export type RelayClient = RelayRequest & RelayRoutes; type RelayRequest = { url: string; getUrl: (endpoint: string) => string; request: (input: RequestInfo | URL, init?: RequestInit) => Promise>; }; type RelayRoutes = { [TKey in keyof TRoutes]: TRoutes[TKey] extends Route ? HasPayload extends true ? ( payload: Prettify< (TRoutes[TKey]["state"]["params"] extends ZodObject ? { params: TRoutes[TKey]["$params"] } : {}) & (TRoutes[TKey]["state"]["query"] extends ZodObject ? { query: TRoutes[TKey]["$query"] } : {}) & (TRoutes[TKey]["state"]["body"] extends ZodType ? { body: TRoutes[TKey]["$body"] } : {}) & { headers?: HeadersInit; } >, ) => RouteResponse : (payload?: { headers: HeadersInit }) => RouteResponse : TRoutes[TKey] extends Routes ? RelayRoutes : never; }; type HasPayload = TRoute["state"]["params"] extends ZodObject ? true : TRoute["state"]["query"] extends ZodObject ? true : TRoute["state"]["body"] extends ZodType ? true : false; type RouteResponse = Promise, RouteErrors>> & { $params: TRoute["$params"]; $query: TRoute["$query"]; $body: TRoute["$body"]; $response: TRoute["$response"]; }; type RouteOutput = TRoute["state"]["output"] extends ZodType ? z.infer : null; type RouteErrors = InstanceType; type Config = { adapter: RelayAdapter; }; type Prettify = { [K in keyof T]: T[K] } & {};