/** * Query options factory for TanStack Query integration. * Creates type-safe query options from Eden route definitions. */ import type { DataTag, DefinedInitialDataOptions, QueryFunction, SkipToken, UndefinedInitialDataOptions, UnusedSkipTokenOptions, } from "@tanstack/react-query" import { queryOptions, skipToken } from "@tanstack/react-query" import { getQueryKey } from "../keys/queryKey" import type { EdenQueryKey } from "../keys/types" // ============================================================================ // Input Types // ============================================================================ /** Reserved options that are set by the library */ type ReservedOptions = "queryKey" | "queryFn" | "queryHashFn" | "queryHash" /** Base options for Eden requests */ interface EdenQueryBaseOptions { eden?: { /** Abort request on component unmount */ abortOnUnmount?: boolean } } /** Result metadata added to query options */ export interface EdenQueryOptionsResult { eden: { path: string } } /** * Arguments for creating query options. */ export interface EdenQueryOptionsArgs { /** Path segments (e.g., ['api', 'users', 'get']) */ path: string[] /** Input parameters or skipToken */ input: TInput | SkipToken /** Function to fetch data */ fetch: (input: TInput, signal?: AbortSignal) => Promise } // ============================================================================ // Input Option Types // ============================================================================ interface UndefinedEdenQueryOptionsIn extends Omit< UndefinedInitialDataOptions, ReservedOptions >, EdenQueryBaseOptions {} interface DefinedEdenQueryOptionsIn extends Omit< DefinedInitialDataOptions< NoInfer, TError, TData, EdenQueryKey >, ReservedOptions >, EdenQueryBaseOptions {} interface UnusedSkipTokenEdenQueryOptionsIn extends Omit< UnusedSkipTokenOptions, ReservedOptions >, EdenQueryBaseOptions {} // ============================================================================ // Output Option Types // ============================================================================ interface UndefinedEdenQueryOptionsOut extends UndefinedInitialDataOptions< TQueryFnData, TError, TData, EdenQueryKey >, EdenQueryOptionsResult { queryKey: DataTag } interface DefinedEdenQueryOptionsOut extends DefinedInitialDataOptions, EdenQueryOptionsResult { queryKey: DataTag } interface UnusedSkipTokenEdenQueryOptionsOut extends UnusedSkipTokenOptions, EdenQueryOptionsResult { queryKey: DataTag } // ============================================================================ // Union Types // ============================================================================ type AnyEdenQueryOptionsIn = | UndefinedEdenQueryOptionsIn | DefinedEdenQueryOptionsIn | UnusedSkipTokenEdenQueryOptionsIn type AnyEdenQueryOptionsOut = | UndefinedEdenQueryOptionsOut | DefinedEdenQueryOptionsOut | UnusedSkipTokenEdenQueryOptionsOut // ============================================================================ // Function Overloads // ============================================================================ /** * Create query options with defined initial data. * The returned data will never be undefined. */ export function edenQueryOptions( args: EdenQueryOptionsArgs & { opts: DefinedEdenQueryOptionsIn }, ): DefinedEdenQueryOptionsOut /** * Create query options without skipToken. * The returned data can be undefined until loaded. */ export function edenQueryOptions( args: EdenQueryOptionsArgs & { input: TInput opts?: UnusedSkipTokenEdenQueryOptionsIn }, ): UnusedSkipTokenEdenQueryOptionsOut /** * Create query options with skipToken support. * Use skipToken to conditionally disable the query. */ export function edenQueryOptions( args: EdenQueryOptionsArgs & { opts?: UndefinedEdenQueryOptionsIn }, ): UndefinedEdenQueryOptionsOut // ============================================================================ // Implementation // ============================================================================ /** * Creates TanStack Query options for an Eden route. * * @example * ```typescript * const options = edenQueryOptions({ * path: ['api', 'users', 'get'], * input: { id: '1' }, * fetch: async (input, signal) => { * const response = await edenClient.api.users.get({ query: input, fetch: { signal } }) * return response.data * }, * }) * * // Use with useQuery * const { data } = useQuery(options) * * // Or prefetch * await queryClient.prefetchQuery(options) * ``` */ export function edenQueryOptions(args: { path: string[] input: TInput | SkipToken fetch: (input: TInput, signal?: AbortSignal) => Promise opts?: AnyEdenQueryOptionsIn }): AnyEdenQueryOptionsOut { const { path, input, fetch: fetchFn, opts } = args const inputIsSkipToken = input === skipToken const queryKey = getQueryKey({ path, input: inputIsSkipToken ? undefined : input, type: "query", }) // Use unknown internally - types are enforced by function signature const queryFn: QueryFunction = async (context) => { const actualInput = input as TInput // Pass abort signal from query context // If eden.abortOnUnmount is true, use the signal const signal = opts?.eden?.abortOnUnmount ? context.signal : undefined const result = await fetchFn(actualInput, signal) return result } // Extract our custom eden options before passing to queryOptions const { eden: _edenOpts, ...tanstackOpts } = opts ?? {} // Build result - types are enforced by function overloads return Object.assign( queryOptions({ ...tanstackOpts, queryKey, queryFn: inputIsSkipToken ? skipToken : queryFn, } as Parameters[0]), { eden: { path: path.join("."), }, }, ) as AnyEdenQueryOptionsOut }