kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
233 lines (227 loc) • 7.86 kB
JavaScript
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 };