UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

233 lines (227 loc) 7.86 kB
import { n as defaultIsUnauthorized } from "../error-Bvo7YEhk.js"; import { n as getFuncRef, r as getFunctionMeta, t as buildMetaIndex } from "../meta-utils-D9K4fICl.js"; import { i as decodeWire, s as getTransformer } from "../transformer-C6pGVHqx.js"; import { n as convexInfiniteQueryOptions, r as convexQuery } from "../query-options-C96zLANM.js"; import { convexToJson } from "convex/values"; import { getFunctionName } from "convex/server"; import { fetchAction, fetchQuery } from "convex/nextjs"; import { hashKey } from "@tanstack/query-core"; //#region src/rsc/http-server.ts /** * Build query options for an HTTP route (server-side). * Does NOT include queryFn - execution handled by getServerQueryClientOptions. */ function buildHttpQueryOptions(route, routeKey, args) { return { queryKey: [ "httpQuery", routeKey, args ], meta: { path: route.path, method: route.method } }; } /** * Execute an HTTP route fetch. * Called by getServerQueryClientOptions queryFn. */ async function fetchHttpRoute(convexSiteUrl, routeMeta, args, token, transformer) { const url = buildUrl(convexSiteUrl, routeMeta.path, args); const response = await fetch(url, { method: routeMeta.method, headers: { "Content-Type": "application/json", ...token ? { Authorization: `Bearer ${token}` } : {} } }); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); if (response.headers.get("content-length") === "0" || response.status === 204) return null; return decodeWire(await response.json(), transformer); } /** * Build URL with path params and query params. */ function buildUrl(convexSiteUrl, pathTemplate, args) { const remaining = { ...args }; const path = pathTemplate.replace(/:(\w+)/g, (_, key) => { const value = remaining[key]; delete remaining[key]; return value !== null && value !== void 0 ? encodeURIComponent(String(value)) : ""; }); const queryEntries = Object.entries(remaining).filter(([_, v]) => v !== void 0 && v !== null); if (queryEntries.length > 0) { const params = new URLSearchParams(); for (const [key, value] of queryEntries) params.set(key, String(value)); return `${convexSiteUrl}${path}?${params.toString()}`; } return convexSiteUrl + path; } //#endregion //#region src/rsc/proxy-server.ts /** * Server-compatible CRPC Proxy for RSC * * Provides a proxy that works in React Server Components. * Query execution is delegated to getServerQueryClientOptions. */ function createRecursiveProxy(api, path, meta) { return new Proxy(() => {}, { get(_target, prop) { if (typeof prop === "symbol") return; if (prop === "then") return; if (path[0] === "http" && prop === "queryOptions") { const routeKey = path.slice(1).join("."); const route = meta._http?.[routeKey]; if (!route) throw new Error(`HTTP route not found: ${routeKey}`); return (args = {}) => buildHttpQueryOptions(route, routeKey, args); } if (prop === "queryOptions") return (args = {}, opts) => { return convexQuery(getFuncRef(api, path), args, meta, opts); }; if (prop === "infiniteQueryOptions") return (args = {}, opts = {}) => { return convexInfiniteQueryOptions(getFuncRef(api, path), args, opts, meta); }; if (prop === "infiniteQueryKey") return (args) => { return [ "convexQuery", getFunctionName(getFuncRef(api, path)), args ?? {} ]; }; if (prop === "meta" && path.length >= 2) return getFunctionMeta(path, meta); return createRecursiveProxy(api, [...path, prop], meta); } }); } /** * Create a server-compatible CRPC proxy for RSC. * Only supports queryOptions (no mutations in RSC). * * Query execution (including auth) is handled by getServerQueryClientOptions. * * @example * ```tsx * // src/lib/convex/rsc.tsx * import { api } from '@convex/api'; * * // Proxy just builds query options - no auth config here * export const crpc = createServerCRPCProxy({ api }); * * // Auth + execution config in QueryClient * const queryClient = new QueryClient({ * defaultOptions: getServerQueryClientOptions({ * getToken: caller.getToken, * convexSiteUrl: env.NEXT_PUBLIC_CONVEX_SITE_URL, * }), * }); * * // app/page.tsx (RSC) * prefetch(crpc.posts.list.queryOptions()); * prefetch(crpc.http.health.queryOptions({})); * ``` */ function createServerCRPCProxy(options) { const { api } = options; return createRecursiveProxy(api, [], buildMetaIndex(api)); } //#endregion //#region src/internal/query-key.ts /** * Shared query key utilities for Convex + TanStack Query. * This file has NO React dependencies so it can be imported in both * server (RSC) and client contexts. */ /** * Check if query key is for a Convex query function. * Format: ['convexQuery', 'namespace:functionName', { args }] */ function isConvexQuery(queryKey) { return queryKey.length >= 2 && queryKey[0] === "convexQuery"; } /** * Check if query key is for a Convex action function. * Format: ['convexAction', 'namespace:functionName', { args }] */ function isConvexAction(queryKey) { return queryKey.length >= 2 && queryKey[0] === "convexAction"; } /** * Create stable hash for Convex query keys. * Uses Convex's JSON serialization for consistent argument hashing. */ function hashConvexQuery(queryKey) { const [, funcName, args] = queryKey; return `convexQuery|${funcName}|${JSON.stringify(convexToJson(args))}`; } /** * Create stable hash for Convex action keys. * Uses Convex's JSON serialization for consistent argument hashing. */ function hashConvexAction(queryKey) { const [, funcName, args] = queryKey; return `convexAction|${funcName}|${JSON.stringify(convexToJson(args))}`; } //#endregion //#region src/internal/hash.ts /** * Create a hash function for TanStack Query that handles Convex keys. */ function createHashFn(fallback = hashKey) { return (queryKey) => { if (isConvexQuery(queryKey)) return hashConvexQuery(queryKey); if (isConvexAction(queryKey)) return hashConvexAction(queryKey); return fallback(queryKey); }; } //#endregion //#region src/rsc/server-query-client.ts /** * Get server QueryClient options for RSC prefetching. * Handles both WebSocket queries (convexQuery/convexAction) and HTTP routes (httpQuery). * * @example * ```ts * const queryClient = new QueryClient({ * defaultOptions: { * ...getServerQueryClientOptions({ * getToken: caller.getToken, * convexSiteUrl: env.NEXT_PUBLIC_CONVEX_SITE_URL, * }), * }, * }); * ``` */ function getServerQueryClientOptions({ getToken, convexSiteUrl, transformer: transformerOptions } = {}) { const transformer = getTransformer(transformerOptions); return { queries: { staleTime: 3e4, queryFn: async ({ queryKey, meta }) => { const [type, ...rest] = queryKey; const token = await getToken?.(); if (type === "httpQuery") { const [routeKey, args] = rest; const routeMeta = meta; if (!convexSiteUrl) throw new Error("convexSiteUrl required for HTTP queries. Pass it to getServerQueryClientOptions()."); if (!routeMeta?.path) throw new Error(`HTTP route metadata missing for: ${routeKey}`); return await fetchHttpRoute(convexSiteUrl, routeMeta, args, token, transformer); } const [funcRef, args] = rest; const wireArgs = transformer.input.serialize(args); const queryMeta = meta; const skipUnauth = queryMeta?.skipUnauth; const authRequired = queryMeta?.authType === "required"; if (!token && (skipUnauth || authRequired)) return null; const opts = token ? { token } : void 0; try { return transformer.output.deserialize(type === "convexQuery" ? await fetchQuery(funcRef, wireArgs, opts) : await fetchAction(funcRef, wireArgs, opts)); } catch (error) { if ((skipUnauth || authRequired) && defaultIsUnauthorized(error)) return null; throw error; } }, queryKeyHashFn: createHashFn() } }; } //#endregion export { createServerCRPCProxy, getServerQueryClientOptions };