import type { AnyElysia, RouteSchema } from 'elysia' import { EdenClient } from './client' import type { TypeError } from './errors' import { type HttpMutationMethod, type HttpQueryMethod, type HttpSubscriptionMethod } from './http' import type { InferRouteBody, InferRouteOptions, InferRouteResponse } from './infer' import { httpLink, type HTTPLinkOptions } from './links' import { parsePathsAndMethod } from './path' import { type ExtractEdenTreatyRouteParams, type ExtractEdenTreatyRouteParamsInput, getPathParam, } from './path-params' import type { EdenRequestOptions } from './request' import type { EdenRequestParams } from './resolve' import type { EmptyToVoid } from './utils/empty-to-void' import { isGetOrHeadMethod, isHttpMethod } from './utils/http' import type { Optional } from './utils/optional' import type { EdenWS } from './ws' /** * RPC proxy derived from {@link AnyElysia._routes} for accessing an Elysia.js API. */ export type EdenTreatyClient = T extends { _routes: infer TSchema extends Record } ? EdenTreatyHooksProxy : TypeError<'Please install Elysia before using Eden'> /** * Recursively iterate over all keys in {@link AnyElyisa._routes}, processing path parameters * and regular path segments separately. * * Regular path parameters will be mapped to a nested object, and then intersected * with anything generated by dynamic path parameters. * * @template TSchema The current level of {@link AnyElysia._routes} being processed. * @template TPath The current path segments up to this point (excluding dynamic path parameters). * @template TRouteParams Keys that are considered path parameters instead of regular path segments. */ export type EdenTreatyHooksProxy< TSchema extends Record, TPath extends any[] = [], TRouteParams = ExtractEdenTreatyRouteParams, > = EdenTreatyPathHooks & EdenTreatyHooksPathParameterHook /** * Recursively handle regular path segments (i.e. NOT path parameters). * * If the value is a {@link RouteSchema}, then it's a "leaf" that does not need to be * recursively processed. The result should be the key, an HTTP method, mapped to a * strongly-typed function. * * @template TSchema The current level of {@link AnyElysia._routes} being processed. * @template TPath The current path segments up to this point (excluding dynamic path parameters). * @template TRouteParams Keys that are considered path parameters instead of regular path segments. */ type EdenTreatyPathHooks< TSchema extends Record, TPath extends any[] = [], TRouteParams = ExtractEdenTreatyRouteParams, > = { [K in Exclude]: TSchema[K] extends RouteSchema ? EdenTreatyQueryRouteLeaf : EdenTreatyHooksProxy } /** * {@link EdenTreatyHooksProxy} intersects the object created by {@link EdenTreatyPathHooks} * for regular path parameters with anything created by this type for dynamic path parameters. * * If there are no dynamic path parameters, then return an empty object. * Intersecting with empty object does nothing. * * Otherwise, return a function that returns the next level of the proxy, omitting * the current dynamic path parameter. * * @template TSchema The current level of {@link AnyElysia._routes} being processed. * @template TPath The current path segments up to this point (excluding dynamic path parameters). * @template TRouteParams Keys that are considered path parameters instead of regular path segments. */ type EdenTreatyHooksPathParameterHook< TSchema extends Record, TPath extends any[] = [], TRouteParams = {}, > = {} extends TRouteParams ? {} : ( params: ExtractEdenTreatyRouteParamsInput, ) => EdenTreatyHooksProxy], TPath> /** * When a {@link RouteSchema} is found, map it to leaves and stop recursive processing. * Leaves are function calls that abstract the native {@link fetch} API. * * Based on the HTTP request "category", e.g. "query", "mutation", "subscription", or "unknown", * return the corresponding leaf. * * @template TRoute The {@link RouteSchema} that was found. * @template TMethod The most recent key that was mapped to the {@link TRoute}. e.g. "get", "post", etc. */ export type EdenTreatyQueryRouteLeaf< TRoute extends RouteSchema, TMethod, > = TMethod extends HttpQueryMethod ? EdenTreatyQueryLeaf : TMethod extends HttpMutationMethod ? EdenTreatyMutationLeaf : TMethod extends HttpSubscriptionMethod ? EdenTreatySubscriptionLeaf : EdenTreatyUnknownLeaf /** * Strongly-typed function for queries (i.e. "GET" requests). */ export type EdenTreatyQueryLeaf = ( options: EmptyToVoid, 'params'>>, ) => Promise> /** * Strongly-typed function for mutations (i.e. "POST", "PATCH", etc. requests). */ export type EdenTreatyMutationLeaf = ( body: EmptyToVoid>, options: EmptyToVoid>, ) => Promise> /** * Strongly-typed function for subscriptions (i.e. "CONNECT", "SUBSCRIBE", etc. requests). * * @TODO: Available hooks assuming that the route supports `createSubscription`. */ export type EdenTreatySubscriptionLeaf = ( options: EmptyToVoid, 'params'>>, ) => EdenWS /** * Strongly-typed function for unknown request. * * @todo What should it actually be... */ export type EdenTreatyUnknownLeaf = EdenTreatyQueryLeaf & EdenTreatyQueryLeaf & EdenTreatyMutationLeaf & EdenTreatySubscriptionLeaf /** * @param client * * @param config * * @param [paths=[]] Path parameter strings including the current path parameter as a placeholder. * @example [ 'products', ':id', ':cursor' ] * * @param [pathParams=[]] An array of objects representing path parameter replacements. * @example [ { id: 123 }, { cursor: '456' } ] */ export function createEdenTreatyProxy( client: EdenClient, config?: EdenRequestOptions, paths: string[] = [], pathParams: Record[] = [], ) { const edenTreatyProxy = new Proxy(() => {}, { get: (_target, path: string, _receiver): any => { // Copy the paths so that it will not be mutated in a nested proxy. // Only add the current path if is not "index". const nextPaths = path === 'index' ? [...paths] : [...paths, path] // Return a nested proxy that has the new paths. return createEdenTreatyProxy(client, config, nextPaths, pathParams) }, apply: (_target, _thisArg, args) => { // Parse the information from the paths array up to this point. const { path, method } = parsePathsAndMethod(paths) // Determine if the current args could be specifying dynamic path parameters. const pathParam = getPathParam(args) // If it is a valid path parameter argument and the HTTP method is not recognized, // then return a nested proxy that includes the path parameter replacement. if (pathParam?.key != null && !isHttpMethod(method)) { const allPathParams = [...pathParams, pathParam.param] const pathsWithParams = [...paths, `:${pathParam.key}`] return createEdenTreatyProxy(client, config, pathsWithParams, allPathParams) } // Otherwise, assume that this is intended to be a request and handle it. let options: any = undefined let body: any = undefined if (isGetOrHeadMethod(method)) { options = args[0] } else { body = args[0] options = args[1] } const params: EdenRequestParams = { body, options, path, method, ...config, } return client.query(params) }, }) return edenTreatyProxy } /** * @param clientOrHttpLinkOptions Either an untyped {@link EdenClient} or options for an httpLink to initialize a default client. * @param options Request options. */ export function createEdenTreaty( clientOrHttpLinkOptions?: EdenClient | HTTPLinkOptions, options?: EdenRequestOptions, ): EdenTreatyClient { if (clientOrHttpLinkOptions instanceof EdenClient) { const proxy = createEdenTreatyProxy(clientOrHttpLinkOptions, options) return proxy as any } const defaultClient = new EdenClient({ links: [httpLink(clientOrHttpLinkOptions)] }) const proxy = createEdenTreatyProxy(defaultClient, options) return proxy as any }