import { Endpoint } from "./endpoint";
// import { Tab, Tabs } from "fumadocs-ui/components/tabs";
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,
};
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 });
const serverBody = createServerBody({
props,
method: method ?? "GET",
requireSession: requireSession ?? false,
forceAsQuery,
forceAsBody,
});
const serverCodeBlock = (
{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,
};
}
const indentationSpace = ` `;
function createClientBody({ props }: { props: Property[] }) {
let body = ``;
let i = -1;
for (const prop of props) {
i++;
if (prop.isServerOnly) continue;
if (body === "") body += "{\n";
let addComment = false;
let comment: string[] = [];
if (!prop.isOptional || prop.comments) addComment = true;
if (!prop.isOptional) comment.push("required");
if (prop.comments) comment.push(prop.comments);
body += `${indentationSpace.repeat(prop.path.length + 1)}${prop.propName}${
prop.exampleValue ? `: ${prop.exampleValue}` : ""
}${prop.type === "Object" ? ": {" : ","}${
addComment ? ` // ${comment.join(", ")}` : ""
}\n`;
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()) {
body += `${indentationSpace.repeat(index + 1)}},\n`;
}
}
}
if (body !== "") body += "}";
return body;
}
function createServerBody({
props,
requireSession,
method,
forceAsBody,
forceAsQuery,
}: {
props: Property[];
requireSession: boolean;
method: string;
forceAsQuery: boolean | undefined;
forceAsBody: boolean | undefined;
}) {
let serverBody = "";
let body2 = ``;
let i = -1;
for (const prop of props) {
i++;
if (prop.isClientOnly) continue;
if (body2 === "") body2 += "{\n";
let addComment = false;
let comment: string[] = [];
if (!prop.isOptional || prop.comments) {
addComment = true;
}
if (
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],
)
)
) {
comment.push("server-only");
addComment = true;
}
if (!prop.isOptional) comment.push("required");
if (prop.comments) comment.push(prop.comments);
body2 += `${indentationSpace.repeat(prop.path.length + 2)}${prop.propName}${
prop.exampleValue ? `: ${prop.exampleValue}` : ""
}${prop.type === "Object" ? ": {" : ","}${
addComment ? ` // ${comment.join(", ")}` : ""
}\n`;
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()) {
body2 += `${indentationSpace.repeat(index + 2)}},\n`;
}
}
}
if (body2 !== "") body2 += " },";
let fetchOptions = "";
if (requireSession) {
fetchOptions +=
"\n // This endpoint requires session cookies.\n headers: await headers(),";
}
if (props.filter((x) => !x.isClientOnly).length > 0) {
serverBody += "{\n";
if ((method === "POST" || forceAsBody) && !forceAsQuery) {
serverBody += ` body: ${body2}${fetchOptions}\n}`;
} else {
serverBody += ` query: ${body2}${fetchOptions}\n}`;
}
} else if (fetchOptions.length) {
serverBody += `{${fetchOptions}\n}`;
}
return serverBody;
}
function Note({ children }: { children: ReactNode }) {
return (
{children as any}