import { Endpoint } from "./endpoint"; import { DynamicCodeBlock } from "./ui/dynamic-code-block"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "./ui/table"; import { ApiMethodTabs, ApiMethodTabsContent, ApiMethodTabsList, ApiMethodTabsTrigger, } from "./api-method-tabs"; import { JSX, ReactNode } from "react"; import { Link } from "lucide-react"; import { Button } from "./ui/button"; import { cn } from "@/lib/utils"; type Property = { isOptional: boolean; description: string | null; propName: string; type: string; exampleValue: string | null; comments: string | null; isServerOnly: boolean; path: string[]; isNullable: boolean; isClientOnly: boolean; }; const placeholderProperty: Property = { isOptional: false, comments: null, description: null, exampleValue: null, propName: "", type: "", isServerOnly: false, path: [], isNullable: false, isClientOnly: false, }; const indentationSpace = ` `; export const APIMethod = ({ path, isServerOnly, isClientOnly, method, children, noResult, requireSession, note, clientOnlyNote, serverOnlyNote, resultVariable = "data", forceAsBody, forceAsQuery, }: { /** * Endpoint path */ path: string; /** * If enabled, we will add `headers` to the fetch options, indicating the given API method requires auth headers. * * @default false */ requireSession?: boolean; /** * The HTTP method to the endpoint * * @default "GET" */ method?: "POST" | "GET" | "DELETE" | "PUT"; /** * Wether the endpoint is server only or not. * * @default false */ isServerOnly?: boolean; /** * Wether the code example is client-only, thus maening it's an endpoint. * * @default false */ isClientOnly?: boolean; /** * The `ts` codeblock which describes the API method. * I recommend checking other parts of the Better-Auth docs which is using this component to get an idea of how to * write out the children. */ children: JSX.Element; /** * If enabled, will remove the `const data = ` part, since this implies there will be no return data from the API method. */ noResult?: boolean; /** * A small note to display above the client-auth example code-block. */ clientOnlyNote?: string; /** * A small note to display above the server-auth example code-block. */ serverOnlyNote?: string; /** * A small note to display above both the client & server auth example code-blocks. */ note?: string; /** * The result output variable name. * * @default "data" */ resultVariable?: string; /** * Force the server auth API to use `body`, rather than auto choosing */ forceAsBody?: boolean; /** * Force the server auth API to use `query`, rather than auto choosing */ forceAsQuery?: boolean; }) => { let { props, functionName, code_prefix, code_suffix } = parseCode(children); const authClientMethodPath = pathToDotNotation(path); const clientBody = createClientBody({ props, method: method ?? "GET", forceAsBody, forceAsQuery, }); const serverBody = createServerBody({ props, method: method ?? "GET", requireSession: requireSession ?? false, forceAsQuery, forceAsBody, }); const serverCodeBlock = ( ); let pathId = path.replaceAll("/", "-"); return ( <>
Client Server
{isServerOnly ? null : ( )} {clientOnlyNote || note ? ( {note && tsxifyBackticks(note)} {clientOnlyNote ? ( <> {note ?
: null} {tsxifyBackticks(clientOnlyNote)} ) : null}
) : null}
{isServerOnly ? (
This is a server-only endpoint
) : null}
{!isServerOnly ? : null}
{isClientOnly ? null : ( )} {serverOnlyNote || note ? ( {note && tsxifyBackticks(note)} {serverOnlyNote ? ( <> {note ?
: null} {tsxifyBackticks(serverOnlyNote)} ) : null}
) : null}
{serverCodeBlock} {isClientOnly ? (
This is a client-only endpoint
) : null}
{!isClientOnly ? : null}
); }; function pathToDotNotation(input: string): string { return input .split("/") // split into segments .filter(Boolean) // remove empty strings (from leading '/') .map((segment) => segment .split("-") // split kebab-case .map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1), ) .join(""), ) .join("."); } function getChildren( x: | ({ props: { children: string } } | string) | ({ props: { children: string } } | string)[], ): string[] { if (Array.isArray(x)) { const res = []; for (const item of x) { res.push(getChildren(item)); } return res.flat(); } else { if (typeof x === "string") return [x]; return [x.props.children]; } } function TypeTable({ props, isServer, }: { props: Property[]; isServer: boolean; }) { if (!isServer && !props.filter((x) => !x.isServerOnly).length) return null; if (isServer && !props.filter((x) => !x.isClientOnly).length) return null; if (!props.length) return null; return ( Prop Description Type {props.map((prop, i) => (prop.isServerOnly && isServer === false) || (prop.isClientOnly && isServer === true) ? null : ( {prop.path.join(".") + (prop.path.length ? "." : "")} {prop.propName} {prop.isOptional ? "?" : ""} {prop.isServerOnly ? ( (server-only) ) : null}
{tsxifyBackticks(prop.description ?? "")}
{prop.type} {prop.isNullable ? " | null" : ""}
), )}
); } function tsxifyBackticks(input: string): JSX.Element { const parts = input.split(/(`[^`]+`)/g); // Split by backtick sections return ( <> {parts.map((part, index) => { if (part.startsWith("`") && part.endsWith("`")) { const content = part.slice(1, -1); // remove backticks return {content}; } else { return {part}; } })} ); } function parseCode(children: JSX.Element) { // These two variables are essentially taking the `children` JSX shiki code, and converting them to // an array string purely of it's code content. const arrayOfJSXCode = children?.props.children.props.children.props.children .map((x: any) => x === "\n" ? { props: { children: { props: { children: "\n" } } } } : x, ) .map((x: any) => x.props.children) .filter((x: any) => x != null); const arrayOfCode: string[] = arrayOfJSXCode .flatMap( ( x: { props: { children: string } } | { props: { children: string } }[], ) => { return getChildren(x); }, ) .join("") .split("\n"); let props: Property[] = []; let functionName: string = ""; let currentJSDocDescription: string = ""; let withinApiMethodType = false; let hasAlreadyDefinedApiMethodType = false; let isServerOnly_ = false; let isClientOnly_ = false; let nestPath: string[] = []; // str arr segmented-path, eg: ["data", "metadata", "something"] let serverOnlyPaths: string[] = []; // str arr full-path, eg: ["data.metadata.something"] let clientOnlyPaths: string[] = []; // str arr full-path, eg: ["data.metadata.something"] let isNullable = false; let code_prefix = ""; let code_suffix = ""; for (let line of arrayOfCode) { const originalLine = line; line = line.trim(); if (line === "}" && withinApiMethodType && !nestPath.length) { withinApiMethodType = false; hasAlreadyDefinedApiMethodType = true; continue; } else { if (line === "}" && withinApiMethodType && nestPath.length) { nestPath.pop(); continue; } } if ( line.toLowerCase().startsWith("type") && !hasAlreadyDefinedApiMethodType && !withinApiMethodType ) { withinApiMethodType = true; // Will grab the name of the API method function name from: // type createOrganization = { // ^^^^^^^^^^^^^^^^^^ functionName = line.replace("type ", "").split("=")[0].trim(); continue; } if (!withinApiMethodType) { if (!hasAlreadyDefinedApiMethodType) { code_prefix += originalLine + "\n"; } else { code_suffix += "\n" + originalLine + ""; } continue; } if ( line.startsWith("/*") || line.startsWith("*") || line.startsWith("*/") ) { if (line.startsWith("/*")) { continue; } else if (line.startsWith("*/")) { continue; } else { if (line === "*" || line === "* ") continue; line = line.replace("* ", ""); if (line.trim() === "@serverOnly") { isServerOnly_ = true; continue; } else if (line.trim() === "@nullable") { isNullable = true; continue; } else if (line.trim() === "@clientOnly") { isClientOnly_ = true; continue; } currentJSDocDescription += line + " "; } } else { // New property field // Example: // name: string = "My Organization", let propName = line.split(":")[0].trim(); const isOptional = propName.endsWith("?") ? true : false; if (isOptional) propName = propName.slice(0, -1); // Remove `?` in propname. let propType = line .replace(propName, "") .replace("?", "") .replace(":", "") .split("=")[0] .trim()!; let isTheStartOfNest = false; if (propType === "{") { // This means that it's a nested object. propType = `Object`; isTheStartOfNest = true; nestPath.push(propName); if (isServerOnly_) { serverOnlyPaths.push(nestPath.join(".")); } if (isClientOnly_) { clientOnlyPaths.push(nestPath.join(".")); } } if (clientOnlyPaths.includes(nestPath.join("."))) { isClientOnly_ = true; } if (serverOnlyPaths.includes(nestPath.join("."))) { isServerOnly_ = true; } let exampleValue = !line.includes("=") ? null : line .replace(propName, "") .replace("?", "") .replace(":", "") .replace(propType, "") .replace("=", "") .trim(); if (exampleValue?.endsWith(",")) exampleValue = exampleValue.slice(0, -1); // const comments = // line // .replace(propName, "") // .replace("?", "") // .replace(":", "") // .replace(propType, "") // .replace("=", "") // .replace(exampleValue || "IMPOSSIBLE_TO_REPLACE_!!!!", "") // .split("//")[1] ?? null; const comments = null; const description = currentJSDocDescription.length > 0 ? currentJSDocDescription : null; if (description) { currentJSDocDescription = ""; } const property: Property = { ...placeholderProperty, description, comments, exampleValue, isOptional, propName, type: propType, isServerOnly: isServerOnly_, isClientOnly: isClientOnly_, path: isTheStartOfNest ? nestPath.slice(0, nestPath.length - 1) : nestPath.slice(), isNullable: isNullable, }; isServerOnly_ = false; isClientOnly_ = false; isNullable = false; // console.log(property); props.push(property); } } return { functionName, props, code_prefix, code_suffix, }; } /** * Builds a property line with proper formatting and comments */ function buildPropertyLine( prop: Property, indentLevel: number, additionalComments: string[] = [], ): string { const comments: string[] = [...additionalComments]; if (!prop.isOptional) comments.push("required"); if (prop.comments) comments.push(prop.comments); const addComment = comments.length > 0; const indent = indentationSpace.repeat(indentLevel); const propValue = prop.exampleValue ? `: ${prop.exampleValue}` : ""; const commentText = addComment ? ` // ${comments.join(", ")}` : ""; if (prop.type === "Object") { // For object types, put comment after the opening brace return `${indent}${prop.propName}${propValue}: {${commentText}\n`; } else { // For non-object types, put comment after the comma return `${indent}${prop.propName}${propValue},${commentText}\n`; } } /** * Determines if the client request should use query parameters * * - GET requests use query params by default, unless `forceAsBody` is true * - Any request can be forced to use query params with `forceAsQuery` */ function shouldClientUseQueryParams( method: string | undefined, forceAsBody: boolean | undefined, forceAsQuery: boolean | undefined, ): boolean { if (forceAsQuery) return true; if (forceAsBody) return false; return method === "GET"; } function createClientBody({ props, method, forceAsBody, forceAsQuery, }: { props: Property[]; method?: string; forceAsBody?: boolean; forceAsQuery?: boolean; }) { const isQueryParam = shouldClientUseQueryParams( method, forceAsBody, forceAsQuery, ); const baseIndentLevel = isQueryParam ? 2 : 1; let params = ``; let i = -1; for (const prop of props) { i++; if (prop.isServerOnly) continue; if (params === "") params += "{\n"; params += buildPropertyLine(prop, prop.path.length + baseIndentLevel); if ((props[i + 1]?.path?.length || 0) < prop.path.length) { const diff = prop.path.length - (props[i + 1]?.path?.length || 0); for (const index of Array(diff) .fill(0) .map((_, i) => i) .reverse()) { params += `${indentationSpace.repeat(index + baseIndentLevel)}},\n`; } } } if (params !== "") { if (isQueryParam) { // Wrap in query object for GET requests and when forceAsQuery is true params = `{\n query: ${params} },\n}`; } else { params += "}"; } } return params; } /** * Determines if the server request should use query parameters * * - GET requests use query params by default, unless `forceAsBody` is true * - Other methods (POST, PUT, DELETE) use body by default, unless `forceAsQuery` is true */ function shouldServerUseQueryParams( method: string, forceAsBody: boolean | undefined, forceAsQuery: boolean | undefined, ): boolean { if (forceAsQuery) return true; if (forceAsBody) return false; return method === "GET"; } function createServerBody({ props, requireSession, method, forceAsBody, forceAsQuery, }: { props: Property[]; requireSession: boolean; method: string; forceAsQuery: boolean | undefined; forceAsBody: boolean | undefined; }) { const isQueryParam = shouldServerUseQueryParams( method, forceAsBody, forceAsQuery, ); const clientOnlyProps = props.filter((x) => !x.isClientOnly); // Build properties content let propertiesContent = ``; let i = -1; for (const prop of props) { i++; if (prop.isClientOnly) continue; if (propertiesContent === "") propertiesContent += "{\n"; // Check if this is a server-only nested property const isNestedServerOnlyProp = prop.isServerOnly && !( prop.path.length && props.find( (x) => x.path.join(".") === prop.path.slice(0, prop.path.length - 2).join(".") && x.propName === prop.path[prop.path.length - 1], ) ); const additionalComments: string[] = []; if (isNestedServerOnlyProp) additionalComments.push("server-only"); propertiesContent += buildPropertyLine( prop, prop.path.length + 2, additionalComments, ); if ((props[i + 1]?.path?.length || 0) < prop.path.length) { const diff = prop.path.length - (props[i + 1]?.path?.length || 0); for (const index of Array(diff) .fill(0) .map((_, i) => i) .reverse()) { propertiesContent += `${indentationSpace.repeat(index + 2)}},\n`; } } } if (propertiesContent !== "") propertiesContent += " },"; // Build fetch options let fetchOptions = ""; if (requireSession) { fetchOptions += "\n // This endpoint requires session cookies.\n headers: await headers(),"; } // Assemble final result let result = ""; if (clientOnlyProps.length > 0) { result += "{\n"; const paramType = isQueryParam ? "query" : "body"; result += ` ${paramType}: ${propertiesContent}${fetchOptions}\n}`; } else if (fetchOptions.length) { result += `{${fetchOptions}\n}`; } return result; } function Note({ children }: { children: ReactNode }) { return (
Notes

{children as any}

); }