mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-22 14:21:55 -05:00
945 lines
24 KiB
TypeScript
945 lines
24 KiB
TypeScript
import { Link } from "lucide-react";
|
|
import type { JSX, ReactNode } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
ApiMethodTabs,
|
|
ApiMethodTabsContent,
|
|
ApiMethodTabsList,
|
|
ApiMethodTabsTrigger,
|
|
} from "./api-method-tabs";
|
|
import { Endpoint } from "./endpoint";
|
|
import { Button } from "./ui/button";
|
|
import { DynamicCodeBlock } from "./ui/dynamic-code-block";
|
|
|
|
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,
|
|
isExternalOnly,
|
|
method,
|
|
children,
|
|
noResult,
|
|
requireSession,
|
|
requireHeaders,
|
|
requireBearerToken,
|
|
headersComment,
|
|
note,
|
|
clientOnlyNote,
|
|
serverOnlyNote,
|
|
resultVariable = "data",
|
|
forceAsBody,
|
|
forceAsParam,
|
|
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;
|
|
/**
|
|
* If enabled, will add a bearer authorization header to the fetch options
|
|
*
|
|
* @default false
|
|
*/
|
|
requireBearerToken?: boolean;
|
|
/**
|
|
* If enabled, we will add `headers` to the fetch options without assuming the request is backed by a session.
|
|
*
|
|
* @default false
|
|
*/
|
|
requireHeaders?: boolean;
|
|
/**
|
|
* A custom comment to display above the generated `headers` line when `requireHeaders` is enabled.
|
|
*/
|
|
headersComment?: string;
|
|
/**
|
|
* 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 meaning it's an endpoint.
|
|
*
|
|
* @default false
|
|
*/
|
|
isClientOnly?: boolean;
|
|
/**
|
|
* Wether the code example is meant for external consumers
|
|
*/
|
|
isExternalOnly?: 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;
|
|
/**
|
|
* Force the server auth api to use `path`, rather than auto choosing
|
|
*/
|
|
forceAsParam?: boolean;
|
|
}) => {
|
|
const { props, functionName, code_prefix, code_suffix } = parseCode(children);
|
|
|
|
const authClientMethodPath = pathToDotNotation(path);
|
|
|
|
const clientBody = createClientBody({
|
|
props,
|
|
method: method ?? "GET",
|
|
forceAsBody,
|
|
forceAsQuery,
|
|
forceAsParam,
|
|
});
|
|
|
|
const serverBody = createServerBody({
|
|
props,
|
|
method: method ?? "GET",
|
|
requireSession: requireSession ?? false,
|
|
requireHeaders: requireHeaders ?? false,
|
|
requireBearerToken: requireBearerToken ?? false,
|
|
headersComment,
|
|
forceAsQuery,
|
|
forceAsParam,
|
|
forceAsBody,
|
|
});
|
|
|
|
const serverCodeBlock = (
|
|
<DynamicCodeBlock
|
|
code={`${code_prefix}${
|
|
noResult ? "" : `const ${resultVariable} = `
|
|
}await auth.api.${functionName}(${serverBody});${code_suffix}`}
|
|
lang="ts"
|
|
allowCopy={!isClientOnly}
|
|
/>
|
|
);
|
|
|
|
const serverTabContent = (
|
|
<div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
|
|
{isClientOnly || isServerOnly ? null : (
|
|
<Endpoint method={method || "GET"} path={path} className="" />
|
|
)}
|
|
{serverOnlyNote || note ? (
|
|
<Note>
|
|
{note && tsxifyBackticks(note)}
|
|
{serverOnlyNote ? (
|
|
<>
|
|
{note ? <br /> : null}
|
|
{tsxifyBackticks(serverOnlyNote)}
|
|
</>
|
|
) : null}
|
|
</Note>
|
|
) : null}
|
|
<div className={cn("relative w-full")}>
|
|
{serverCodeBlock}
|
|
{isClientOnly ? (
|
|
<div className="flex absolute inset-0 justify-center items-center w-full h-full rounded-lg border backdrop-brightness-50 backdrop-blur-xs border-border">
|
|
<span>This is a client-only endpoint</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
{!isClientOnly ? <TypeTable props={props} isServer /> : null}
|
|
</div>
|
|
);
|
|
|
|
if (isExternalOnly) {
|
|
return serverTabContent;
|
|
}
|
|
|
|
const pathId = path.replaceAll("/", "-");
|
|
|
|
return (
|
|
<>
|
|
<div className="relative">
|
|
<div
|
|
id={`api-method${pathId}`}
|
|
aria-hidden
|
|
className="absolute invisible -top-[100px]"
|
|
/>
|
|
</div>
|
|
<ApiMethodTabs
|
|
defaultValue={isServerOnly ? "server" : "client"}
|
|
className="gap-0 w-full"
|
|
>
|
|
<ApiMethodTabsList className="relative flex justify-start w-full p-0 bg-background hover:[&>div>a>button]:opacity-100">
|
|
{(
|
|
[
|
|
{
|
|
value: "client",
|
|
label: "Client",
|
|
icon: (
|
|
<>
|
|
<rect width="20" height="14" x="2" y="3" rx="2" />
|
|
<path d="M8 21h8m-4-4v4" />
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
value: "server",
|
|
label: "Server",
|
|
icon: (
|
|
<>
|
|
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
|
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
|
<path d="M6 6h.01M6 18h.01" />
|
|
</>
|
|
),
|
|
},
|
|
] as const
|
|
).map((tab) => (
|
|
<ApiMethodTabsTrigger
|
|
key={tab.value}
|
|
value={tab.value}
|
|
className="transition-all duration-150 ease-in-out max-w-[100px] data-[state=active]:bg-fd-muted/80 hover:bg-fd-secondary/70 bg-background border hover:border-primary/15 cursor-pointer data-[state=active]:border-primary/10 rounded-none dark:bg-[#050505] dark:hover:bg-[#0a0a0a] dark:data-[state=active]:bg-fd-muted/80 dark:border-white/[0.06] dark:hover:border-white/10 dark:data-[state=active]:border-white/10"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="1em"
|
|
height="1em"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
{tab.icon}
|
|
</svg>
|
|
<span>{tab.label}</span>
|
|
</ApiMethodTabsTrigger>
|
|
))}
|
|
<div className="absolute right-0">
|
|
<a href={`#api-method${pathId}`}>
|
|
<Button
|
|
variant="ghost"
|
|
className="opacity-100 transition-all duration-150 ease-in-out scale-90 md:opacity-0"
|
|
size={"icon"}
|
|
>
|
|
<Link className="size-4" />
|
|
</Button>
|
|
</a>
|
|
</div>
|
|
</ApiMethodTabsList>
|
|
<ApiMethodTabsContent value="client">
|
|
<div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
|
|
{isServerOnly ? null : (
|
|
<Endpoint method={method || "GET"} path={path} />
|
|
)}
|
|
{clientOnlyNote || note ? (
|
|
<Note>
|
|
{note && tsxifyBackticks(note)}
|
|
{clientOnlyNote ? (
|
|
<>
|
|
{note ? <br /> : null}
|
|
{tsxifyBackticks(clientOnlyNote)}
|
|
</>
|
|
) : null}
|
|
</Note>
|
|
) : null}
|
|
<div className={cn("relative w-full")}>
|
|
<DynamicCodeBlock
|
|
code={`${code_prefix}${
|
|
noResult
|
|
? ""
|
|
: `const { data${
|
|
resultVariable === "data" ? "" : `: ${resultVariable}`
|
|
}, error } = `
|
|
}await authClient.${authClientMethodPath}(${clientBody});${code_suffix}`}
|
|
lang="ts"
|
|
allowCopy={!isServerOnly}
|
|
/>
|
|
{isServerOnly ? (
|
|
<div className="flex absolute inset-0 justify-center items-center w-full h-full rounded-lg border backdrop-brightness-50 backdrop-blur-xs border-border">
|
|
<span>This is a server-only endpoint</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
{!isServerOnly ? (
|
|
<TypeTable props={props} isServer={false} />
|
|
) : null}
|
|
</div>
|
|
</ApiMethodTabsContent>
|
|
<ApiMethodTabsContent value="server">
|
|
{serverTabContent}
|
|
</ApiMethodTabsContent>
|
|
</ApiMethodTabs>
|
|
</>
|
|
);
|
|
};
|
|
|
|
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;
|
|
|
|
const filteredProps = props.filter(
|
|
(prop) =>
|
|
!(prop.isServerOnly && isServer === false) &&
|
|
!(prop.isClientOnly && isServer === true),
|
|
);
|
|
|
|
if (!filteredProps.length) return null;
|
|
|
|
return (
|
|
<div className="mt-0">
|
|
<div className="flex items-center gap-2 px-3.5 py-2 border-y border-border bg-fd-muted/80">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="text-muted-foreground"
|
|
>
|
|
<path d="M16 3h5v5" />
|
|
<path d="M8 3H3v5" />
|
|
<path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3" />
|
|
<path d="m15 9 6-6" />
|
|
</svg>
|
|
<span className="text-xs font-medium text-muted-foreground tracking-wider">
|
|
Parameters
|
|
</span>
|
|
</div>
|
|
<PropertyList props={filteredProps} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PropertyItem({ prop }: { prop: Property }) {
|
|
return (
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<code className="text-xs font-semibold text-foreground/90">
|
|
{prop.propName}
|
|
</code>
|
|
<span className="text-xs font-mono text-foreground/60 font-medium">
|
|
{prop.type}
|
|
{prop.isNullable ? " | null" : ""}
|
|
</span>
|
|
{!prop.isOptional && (
|
|
<span className="text-[10px] font-mono font-medium text-amber-600 dark:text-amber-500/80">
|
|
required
|
|
</span>
|
|
)}
|
|
{prop.isServerOnly && (
|
|
<span className="text-[10px] font-mono font-medium text-blue-600 dark:text-blue-400/80">
|
|
server
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PropertyList({
|
|
props,
|
|
nested = false,
|
|
}: {
|
|
props: Property[];
|
|
nested?: boolean;
|
|
}) {
|
|
const groups: { prop: Property; children: Property[] }[] = [];
|
|
let i = 0;
|
|
|
|
while (i < props.length) {
|
|
const prop = props[i];
|
|
if (prop.type === "Object") {
|
|
const parentSegments = [...prop.path, prop.propName];
|
|
const children: Property[] = [];
|
|
i++;
|
|
while (
|
|
i < props.length &&
|
|
props[i].path.length >= parentSegments.length &&
|
|
parentSegments.every((seg, idx) => props[i].path[idx] === seg)
|
|
) {
|
|
children.push(props[i]);
|
|
i++;
|
|
}
|
|
groups.push({ prop, children });
|
|
} else {
|
|
groups.push({ prop, children: [] });
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="divide-y divide-border">
|
|
{groups.map((group) => (
|
|
<div
|
|
key={`${group.prop.path.join(".")}.${group.prop.propName}`}
|
|
className={cn(
|
|
nested ? "px-3 py-3" : "px-3.5 py-3",
|
|
group.children.length > 0 && "pb-3",
|
|
)}
|
|
>
|
|
<PropertyItem prop={group.prop} />
|
|
{group.prop.description && (
|
|
<p className="mt-1 mb-0 text-sm leading-relaxed max-w-xl">
|
|
{tsxifyBackticks(group.prop.description)}
|
|
</p>
|
|
)}
|
|
{group.children.length > 0 && (
|
|
<div className="mt-3 border rounded-md overflow-hidden">
|
|
<PropertyList props={group.children} nested />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 <code key={index}>{content}</code>;
|
|
} else {
|
|
return <span key={index}>{part}</span>;
|
|
}
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
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");
|
|
|
|
const props: Property[] = [];
|
|
|
|
let functionName: string = "";
|
|
let currentJSDocDescription: string = "";
|
|
|
|
let withinApiMethodType = false;
|
|
let hasAlreadyDefinedApiMethodType = false;
|
|
let isServerOnly_ = false;
|
|
let isClientOnly_ = false;
|
|
const nestPath: string[] = []; // str arr segmented-path, eg: ["data", "metadata", "something"]
|
|
const serverOnlyPaths: string[] = []; // str arr full-path, eg: ["data.metadata.something"]
|
|
const 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,
|
|
forceAsParam: boolean | undefined,
|
|
): boolean {
|
|
if (forceAsQuery) return true;
|
|
if (forceAsBody) return false;
|
|
if (forceAsParam) return false;
|
|
return method === "GET";
|
|
}
|
|
|
|
function createClientBody({
|
|
props,
|
|
method,
|
|
forceAsBody,
|
|
forceAsQuery,
|
|
forceAsParam,
|
|
}: {
|
|
props: Property[];
|
|
method?: string;
|
|
forceAsBody?: boolean;
|
|
forceAsQuery?: boolean;
|
|
forceAsParam?: boolean;
|
|
}) {
|
|
const isQueryParam = shouldClientUseQueryParams(
|
|
method,
|
|
forceAsBody,
|
|
forceAsQuery,
|
|
forceAsParam,
|
|
);
|
|
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,
|
|
forceAsParam: boolean | undefined,
|
|
): boolean {
|
|
if (forceAsQuery) return true;
|
|
if (forceAsBody) return false;
|
|
if (forceAsParam) return false;
|
|
return method === "GET";
|
|
}
|
|
|
|
function createServerBody({
|
|
props,
|
|
requireSession,
|
|
requireHeaders,
|
|
requireBearerToken,
|
|
headersComment,
|
|
method,
|
|
forceAsBody,
|
|
forceAsParam,
|
|
forceAsQuery,
|
|
}: {
|
|
props: Property[];
|
|
requireSession: boolean;
|
|
requireHeaders: boolean;
|
|
requireBearerToken: boolean;
|
|
headersComment: string | undefined;
|
|
method: string;
|
|
forceAsQuery: boolean | undefined;
|
|
forceAsParam: boolean | undefined;
|
|
forceAsBody: boolean | undefined;
|
|
}) {
|
|
const isQueryParam = shouldServerUseQueryParams(
|
|
method,
|
|
forceAsBody,
|
|
forceAsQuery,
|
|
forceAsParam,
|
|
);
|
|
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(),";
|
|
} else if (requireHeaders) {
|
|
fetchOptions += `\n // ${
|
|
headersComment ||
|
|
"Pass the current request headers so Better Auth can read and set cookies."
|
|
}\n headers: await headers(),`;
|
|
}
|
|
|
|
if (requireBearerToken) {
|
|
fetchOptions +=
|
|
"\n // This endpoint requires a bearer authentication token.\n headers: { authorization: 'Bearer <token>' },";
|
|
}
|
|
|
|
// Assemble final result
|
|
let result = "";
|
|
if (clientOnlyProps.length > 0) {
|
|
result += "{\n";
|
|
const paramType = isQueryParam ? "query" : forceAsParam ? "params" : "body";
|
|
result += ` ${paramType}: ${propertiesContent}${fetchOptions}\n}`;
|
|
} else if (fetchOptions.length) {
|
|
result += `{${fetchOptions}\n}`;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function Note({ children }: { children: ReactNode }) {
|
|
return (
|
|
<div className="flex relative flex-col gap-2 p-3 mb-2 w-full wrap-break-word rounded-md border-b text-md text-wrap bg-fd-muted/80">
|
|
<span className="-mb-1 w-full text-xs select-none text-foreground/80 font-medium">
|
|
Notes
|
|
</span>
|
|
<p className="mt-0 mb-0 text-sm text-fd-muted-foreground">
|
|
{children as any}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|