UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

216 lines (212 loc) 7.61 kB
import { i as getFunctionType, n as getFuncRef, t as buildMetaIndex } from "./meta-utils-D9K4fICl.js"; import { s as getTransformer } from "./transformer-C6pGVHqx.js"; import { fetchAction, fetchMutation, fetchQuery } from "convex/nextjs"; //#region src/server/caller.ts function createRecursiveProxy(api, path, createOpts) { return new Proxy(() => {}, { get(_target, prop) { if (typeof prop === "symbol") return; if (prop === "then") return; return createRecursiveProxy(api, [...path, prop], createOpts); }, apply(_target, _thisArg, argsList) { const funcRef = getFuncRef(api, path); const args = createOpts.transformer.input.serialize(argsList[0] ?? {}); const callerOpts = argsList[1]; const fnType = getFunctionType(path, createOpts.meta); if (fnType === "query") return createOpts.fetchQuery(funcRef, args, callerOpts).then((result) => createOpts.transformer.output.deserialize(result)); if (fnType === "mutation") return createOpts.fetchMutation(funcRef, args, callerOpts).then((result) => createOpts.transformer.output.deserialize(result)); return createOpts.fetchAction(funcRef, args, callerOpts).then((result) => createOpts.transformer.output.deserialize(result)); } }); } /** * Create a server caller for direct data fetching without React Query. * * This is detached from React Query cache - data is NOT available in client components. * Use for server-only data that doesn't need to be shared with the client. * * @example * ```tsx * // src/lib/convex/rsc.tsx * export const caller = createServerCaller(api, { * fetchQuery: fetchAuthQuery, * fetchMutation: fetchAuthMutation, * }); * * // app/page.tsx (RSC) * const posts = await caller.posts.list(); * return <div>{posts?.length} posts</div>; * ``` */ function createServerCaller(api, opts) { return createRecursiveProxy(api, [], { ...opts, transformer: getTransformer(opts.transformer) }); } //#endregion //#region src/server/lazy-caller.ts /** * Lazy caller that creates context on each procedure invocation. * Matches tRPC's appRouter.createCaller(createTRPCContext) pattern. */ function traverseCaller(caller, path) { let target = caller; for (const key of path) target = target[key]; return target; } function createRecursiveProxyWithLazyContext(api, path, createContext) { return new Proxy(() => {}, { get(_target, prop) { if (typeof prop === "symbol") return; if (prop === "then") return; if (path.length === 0 && prop === "isAuth") return async () => { return (await createContext()).isAuthenticated; }; if (path.length === 0 && prop === "isUnauth") return async () => { return !(await createContext()).isAuthenticated; }; if (path.length === 0 && prop === "getToken") return async () => { return (await createContext()).token; }; return createRecursiveProxyWithLazyContext(api, [...path, prop], createContext); }, async apply(_target, _thisArg, argsList) { try { getFuncRef(api, path); } catch (error) { const displayPath = path.length > 0 ? path.join(".") : "<root>"; throw new Error(`Invalid caller path: ${displayPath}`, { cause: error }); } return traverseCaller((await createContext()).caller, path)(argsList[0] ?? {}, argsList[1]); } }); } /** * Create a lazy caller that creates context on each procedure invocation. * Matches tRPC's `appRouter.createCaller(createTRPCContext)` pattern. * * @example * ```ts * // server.ts * const { createContext, createCaller } = createCallerFactory({...}); * * // rsc.tsx * const createRSCContext = cache(async () => { * const heads = await headers(); * return createContext({ headers: heads }); * }); * export const caller = createCaller(createRSCContext); * * // app/page.tsx - single call! Context created lazily * const posts = await caller.posts.list(); * ``` */ function createLazyCaller(api, createContext) { return createRecursiveProxyWithLazyContext(api, [], createContext); } //#endregion //#region src/server/caller-factory.ts /** biome-ignore-all lint/suspicious/noExplicitAny: lib */ /** * Framework-agnostic caller factory. * getToken is passed as a parameter so framework auth helpers stay decoupled. */ const CONVEX_SITE_URL_RE = /\.convex\.site(?=\/|$)/; const getArgsAndOptions = (args, token, url) => [args[0], { token, url }]; const parseConvexSiteUrl = (url) => { if (!url) throw new Error("CONVEX_SITE_URL is not set. This must be set in the environment."); if (url.endsWith(".convex.cloud")) throw new Error(`CONVEX_SITE_URL should end in .convex.site, not .convex.cloud. Currently set to ${url}.`); return url; }; const getConvexUrl = (siteUrl, convexUrl) => { if (convexUrl) return convexUrl; return siteUrl.replace(CONVEX_SITE_URL_RE, ".convex.cloud"); }; /** * Framework-agnostic caller factory. * * @example * ```ts * const { createContext, createCaller } = createCallerFactory({ * api, * convexSiteUrl: env.NEXT_PUBLIC_CONVEX_SITE_URL, * auth: { getToken }, * }); * ``` */ const noAuthGetToken = () => Promise.resolve({ token: void 0 }); function createCallerFactory(opts) { const siteUrl = parseConvexSiteUrl(opts.convexSiteUrl); const convexUrl = getConvexUrl(siteUrl, opts.convexUrl); const getToken = opts.auth?.getToken ?? noAuthGetToken; const isUnauthorized = opts.auth?.isUnauthorized; const crpcMeta = buildMetaIndex(opts.api); const callWithTokenAndRetry = async (fn, tokenResult, headers) => { const shouldRetryWithFreshToken = !!opts.auth && !tokenResult.isFresh; try { return await fn(tokenResult.token); } catch (error) { if (!shouldRetryWithFreshToken) { if (isUnauthorized?.(error)) return null; throw error; } const newToken = await getToken(siteUrl, headers, { ...opts, forceRefresh: true }); try { return await fn(newToken.token); } catch (retryError) { if (isUnauthorized?.(retryError)) return null; throw retryError; } } }; const createContext = async (reqOpts) => { const tokenResult = await getToken(siteUrl, reqOpts.headers, opts); const fetchAuthQuery = async (query, args, callerOpts) => { if (callerOpts?.skipUnauth && !tokenResult.token) return null; return callWithTokenAndRetry((token) => { const argsAndOptions = getArgsAndOptions([args], token, convexUrl); return fetchQuery(query, argsAndOptions[0], argsAndOptions[1]); }, tokenResult, reqOpts.headers); }; const fetchAuthMutation = async (mutation, args, callerOpts) => { if (callerOpts?.skipUnauth && !tokenResult.token) return null; return callWithTokenAndRetry((token) => { const argsAndOptions = getArgsAndOptions([args], token, convexUrl); return fetchMutation(mutation, argsAndOptions[0], argsAndOptions[1]); }, tokenResult, reqOpts.headers); }; const fetchAuthAction = async (action, args, callerOpts) => { if (callerOpts?.skipUnauth && !tokenResult.token) return null; return callWithTokenAndRetry((token) => { const argsAndOptions = getArgsAndOptions([args], token, convexUrl); return fetchAction(action, argsAndOptions[0], argsAndOptions[1]); }, tokenResult, reqOpts.headers); }; return { caller: createServerCaller(opts.api, { fetchAction: fetchAuthAction, fetchMutation: fetchAuthMutation, fetchQuery: fetchAuthQuery, meta: crpcMeta, transformer: opts.transformer }), isAuthenticated: !!tokenResult.token, token: tokenResult.token }; }; const createCaller = (ctxFn) => createLazyCaller(opts.api, ctxFn); return { createCaller, createContext }; } //#endregion export { createLazyCaller as n, createServerCaller as r, createCallerFactory as t };