import type { EdenClient, EdenCreateClient, EdenRequestOptions, ExtractEdenTreatyRouteParams, ExtractEdenTreatyRouteParamsInput, HttpBatchLinkOptions, HTTPLinkOptions, HttpMutationMethod, HttpQueryMethod, HttpSubscriptionMethod, InferRouteOptions, } from '@aydee-app/eden' import type { AnyElysia, RouteSchema } from 'elysia' import type { Prettify } from 'elysia/types' import type { EdenQueryConfig } from '../../config' import type { EdenContextProps, EdenContextState, EdenProvider } from '../../context' import type { EdenUseInfiniteQuery } from '../../integration/hooks/use-infinite-query' import type { EdenUseMutation } from '../../integration/hooks/use-mutation' import type { EdenUseQuery } from '../../integration/hooks/use-query' import type { EdenUseSuspenseInfiniteQuery } from '../../integration/hooks/use-suspense-infinite-query' import type { EdenUseSuspenseQuery } from '../../integration/hooks/use-suspense-query' import type { InfiniteCursorKey } from '../../integration/internal/infinite-query' import type { EdenMutationKey, EdenQueryKey, EdenQueryKeyOptions, EdenQueryType, } from '../../integration/internal/query-key' import { getMutationKey as internalGetMutationKey, getQueryKey as internalGetQueryKey, } from '../../integration/internal/query-key' import { getPathParam, mutateArgs } from '../../utils/path-param' import type { EdenTreatyReactQueryUtils } from './query-utils' import { createEdenTreatyQueryRootHooks, type EdenTreatyQueryRootHooks } from './root-hooks' import type { EdenTreatyUseQueries } from './use-queries' import type { EdenTreatyUseSuspenseQueries } from './use-suspense-queries' /** * The treaty-query API provides utility methods that are available at the root, as well as * a strongly-typed proxy representing the {@link AnyElysia} API. */ export type EdenTreatyReactQuery = EdenTreatyReactQueryBase< TElysia, TSSRContext > & EdenTreatyReactQueryHooks /** * Utilities available at the eden-treaty + react-query root. */ export interface EdenTreatyReactQueryBase { /** * Get utilities provided via the context API. * * @see https://trpc.io/docs/v11/client/react/useUtils */ useUtils(): EdenTreatyReactQueryUtils /** * Returns everything that will be provided from context. * * e.g. the root utility functions, and root configuration settings. */ createContext( props: EdenContextProps, ): EdenContextState /** * Creates a proxy that can invoke tanstack-query helper functions. */ createUtils( props: EdenContextProps, ): EdenTreatyReactQueryUtils /** * Get utilities provided via the context API. * * @deprecated renamed to {@link useUtils} and will be removed in a future tRPC version * * @link https://trpc.io/docs/v11/client/react/useUtils */ useContext(): EdenTreatyReactQueryUtils /** * React.Provider to set the context. */ Provider: EdenProvider /** * Wraps the `useQueries` react-query hook in a type-safe proxy. */ useQueries: EdenTreatyUseQueries /** * Wraps the `useSuspenseQueries` react-query hook in a type-safe proxy. */ useSuspenseQueries: EdenTreatyUseSuspenseQueries /** * Create a raw, untyped-client. */ createClient: EdenCreateClient /** * Convenience method for creating and configuring a client with a single HTTPLink. */ createHttpClient: (options?: HTTPLinkOptions) => EdenClient /** * Convenience method for creating and configuring a client with a single HttpBatchLink. */ createHttpBatchClient: (options?: HttpBatchLinkOptions) => EdenClient } /** * A strongly-typed proxy with svelte-query hooks for interacting with the {@link AnyElysia} backend. */ export type EdenTreatyReactQueryHooks = T extends { _routes: infer TSchema extends Record } ? EdenTreatyReactQueryHooksProxy : '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 EdenTreatyReactQueryHooksProxy< TSchema extends Record, TPath extends any[] = [], TRouteParams = ExtractEdenTreatyRouteParams, > = EdenTreatyReactQueryPathHooks & EdenTreatyReactQueryHooksPathParameterHook /** * 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 svelte-query hooks. * * @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 EdenTreatyReactQueryPathHooks< TSchema extends Record, TPath extends any[] = [], TRouteParams = ExtractEdenTreatyRouteParams, > = { [K in Exclude]: TSchema[K] extends RouteSchema ? EdenTreatyReactQueryRouteLeaf : EdenTreatyReactQueryHooksProxy } /** * This intersects the object created by {@link EdenTreatyPathHooks} * (for regular path parameters) to handle 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. */ export type EdenTreatyReactQueryHooksPathParameterHook< TSchema extends Record, TPath extends any[] = [], TRouteParams = {}, > = {} extends TRouteParams ? {} : ( params: ExtractEdenTreatyRouteParamsInput, ) => EdenTreatyReactQueryHooksProxy], TPath> /** * When a {@link RouteSchema} is found, map it to leaves and stop recursive processing. * Leaves are objects with svelte-query hooks related to the HTTP method. * * For example, the object would contain hooks for queries if {@link TMethod} was "get". * * @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. * @template TPath The current path segments up to this point (excluding dynamic path parameters). */ export type EdenTreatyReactQueryRouteLeaf< TRoute extends RouteSchema, TMethod, TPath extends any[] = [], > = TMethod extends HttpQueryMethod ? EdenTreatyReactQueryLeaf : TMethod extends HttpMutationMethod ? EdenTreatyReactQueryMutationLeaf : TMethod extends HttpSubscriptionMethod ? EdenTreatyReactQuerySubscriptionLeaf : EdenTreatyReactQueryUnknownLeaf /** * react-query hooks for "infinite-queries". * e.g. routes with a "GET" endpoint that also expects "cursor" as a query parameter. */ export type EdenTreatyReactQueryLeaf< TRoute extends RouteSchema, TPath extends any[] = [], TInput extends InferRouteOptions = InferRouteOptions, > = { useQuery: EdenUseQuery useSuspenseQuery: EdenUseSuspenseQuery } & (InfiniteCursorKey extends keyof (TInput['params'] & TInput['query']) ? EdenTreatyReactQueryInfiniteQueryLeaf : {}) /** * react-query hooks for "infinite-queries". * e.g. routes with a "GET" endpoint that also expects "cursor" as a query parameter. */ export type EdenTreatyReactQueryInfiniteQueryLeaf< TRoute extends RouteSchema, TPath extends any[] = [], > = { useInfiniteQuery: EdenUseInfiniteQuery useSuspenseInfiniteQuery: EdenUseSuspenseInfiniteQuery } /** * react-query hooks for "mutations". e.g. Basically routes with any HTTP methods other than "GET." */ export type EdenTreatyReactQueryMutationLeaf< TRoute extends RouteSchema, TPath extends any[] = [], > = { useMutation: EdenUseMutation } /** * @todo: svelte-query hooks for "subscriptions". e.g. Basically routes that support "CONNECT" or "SUBSCRIBE" requests. * * @see https://github.com/trpc/trpc/blob/52a57eaa9c12394778abf5f0e6b52ec6f46288ed/packages/react-query/src/shared/hooks/createHooksInternal.tsx#L347 * @see https://tkdodo.eu/blog/using-web-sockets-with-react-query */ export type EdenTreatyReactQuerySubscriptionLeaf< TRoute extends RouteSchema, TPath extends any[] = [], TInput = InferRouteOptions, > = { options: Prettify queryKey: EdenQueryKey } /** * react-query hooks for any unknown HTTP methods will just expose all known hooks for now. * * @todo Decide what hooks to expose... */ export type EdenTreatyReactQueryUnknownLeaf< TRoute extends RouteSchema, TPath extends any[] = [], > = EdenTreatyReactQueryLeaf & EdenTreatyReactQueryInfiniteQueryLeaf & EdenTreatyReactQueryMutationLeaf & EdenTreatyReactQuerySubscriptionLeaf export function createEdenTreatyReactQuery( config?: EdenQueryConfig, ): EdenTreatyReactQuery { /** * Root hooks are invoked by leaf nodes in the proxy. * Create the utility functions once at the beginning, and have all leaves use the same one. */ const rootHooks = createEdenTreatyQueryRootHooks(config) /** * The actual proxy. */ const edenTreatyReactQueryProxy = createEdenTreatyReactQueryProxy(rootHooks, config) /** * Wrapper around the proxy that will attempt to return properties found * on the root hooks before accessing the proxy. */ const edenTreatyReactQuery = new Proxy(() => {}, { get: (_target, path: string, _receiver): any => { if (Object.prototype.hasOwnProperty.call(rootHooks, path)) { return rootHooks[path as never] } return edenTreatyReactQueryProxy[path as never] }, }) return edenTreatyReactQuery as any } /** * Creates the recursive proxy. * * @param config Root hooks that were created. * * @param rootHooks The original configuration for eden-treaty. * * @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 createEdenTreatyReactQueryProxy( rootHooks: EdenTreatyQueryRootHooks, config?: EdenQueryConfig, paths: (string | symbol)[] = [], pathParams: Record[] = [], ) { const edenTreatyQueryProxy = new Proxy(() => {}, { get: (_target, path: string, _receiver): any => { const nextPaths = path === 'index' ? [...paths] : [...paths, path] return createEdenTreatyReactQueryProxy(rootHooks, config, nextPaths, pathParams) }, apply: (_target, _thisArg, args) => { /** * @example ['nendoroid', 'createQuery'] */ const pathsCopy = [...paths] /** * @example 'useQuery' */ const hook = pathsCopy.pop() ?? '' /** * Hook that returns path segment array. * * @internal */ if (hook === routeDefinitionSymbol) { return pathsCopy } /** * Determine whether a path parameter can be found from the provided args. * * @example { param: { id: '123' }, key: 'id' } * * The `param` property is the actual argument that was passed, * while they key is the string representing the placeholder. */ const pathParam = getPathParam(args) /** * Determine if the property can be found on the root hooks. * @example "useQuery," "useMutation," etc. */ const isRootProperty = Object.prototype.hasOwnProperty.call(rootHooks, hook) if (pathParam?.key != null && !isRootProperty) { const allPathParams = [...pathParams, pathParam.param] const pathsWithParams = [...paths, `:${pathParam.key}`] return createEdenTreatyReactQueryProxy(rootHooks, config, pathsWithParams, allPathParams) } // There is no option to pass in input from the public exposed hook, // but the internal root `useMutation` hook expects input as the first argument. // Add an empty element at the front representing "input". if (hook === 'useMutation') { args.unshift(undefined) } const modifiedArgs = mutateArgs(hook, args, pathParams) /** * ```ts * // The final hook that was invoked. * const hook = "useQuery" * * // The array of path segments up to this point. * // Note how ":id" is included, this will be replaced by the `resolveRequest` function from eden. * const pathsCopy = ["nendoroid", ":id", "name"] * * // Accummulated path parameters up to this point. * const pathParams = [ { id: 1895 } ] * * // The user provided a search query and query options. * const args = [ { location: "jp" }, { refetchOnUnmount: true } ] * * // The accummulated path parameters and search query are merged into one "input" object. * const modifiedArgs = [ * { query: { location: "jp" }, params: { id: 1895 } }, * { refetchOnMount: false } * ] * * // The full function call contains three arguments: * // array of path segments, input, and query options. * rootHooks.useQuery( * ["nendoroid", ":id", "name"], * { query: { location: "jp" }, params: { id: 1895 } }, * { refetchOnMount: false } * ) * ``` */ const result = (rootHooks as any)[hook](pathsCopy, ...modifiedArgs) return result }, }) return edenTreatyQueryProxy } export function getQueryKey>( route: EdenTreatyReactQueryHooksProxy, input?: TSchema extends RouteSchema ? InferRouteOptions : any, type?: EdenQueryType, ): EdenQueryKey { const paths = (route as any)[routeDefinitionSymbol]() return internalGetQueryKey(paths, input, type ?? 'any') } export function getMutationKey( route: EdenTreatyReactQueryHooksProxy, options?: EdenQueryKeyOptions, ): EdenMutationKey { const paths = (route as any)[routeDefinitionSymbol]() return internalGetMutationKey(paths, options) } export const routeDefinitionSymbol = Symbol('eden-treaty-react-query-defs') export * from './infer' export * from './query-utils' export * from './root-hooks' export * from './use-queries' export * from './use-suspense-queries'