From a357e9a7d2a4eae20815d603313fa1f13fa55319 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Jan 2026 10:59:01 +0000 Subject: [PATCH 1/3] feat: Migrate API reference pages to App Router with Server Components - Create lib/openapi with cached loader, types, and helpers - Create app/(api-reference) route group with layout and providers - Create server components for resource sections, methods, schemas - Create client component islands for expandable properties and code examples - Create catch-all routes for /api-reference and /mapi-reference - Update next.config.js to remove rewrites for API reference pages Co-authored-by: chris --- .../api-reference/[[...slug]]/loading.tsx | 36 ++ .../api-reference/[[...slug]]/page.tsx | 133 +++++++ .../components/expandable-response.tsx | 160 +++++++++ .../api-reference/components/index.ts | 9 + .../components/loading-skeleton.tsx | 47 +++ .../components/method-content.tsx | 254 ++++++++++++++ .../components/multi-lang-example-server.tsx | 88 +++++ .../operation-parameters-server.tsx | 76 ++++ .../api-reference/components/page-shell.tsx | 100 ++++++ .../api-reference/components/pre-content.tsx | 57 +++ .../components/resource-section.tsx | 324 ++++++++++++++++++ .../components/schema-properties-server.tsx | 207 +++++++++++ app/(api-reference)/layout.tsx | 49 +++ .../mapi-reference/[[...slug]]/loading.tsx | 36 ++ .../mapi-reference/[[...slug]]/page.tsx | 144 ++++++++ app/(api-reference)/providers.tsx | 45 +++ lib/openapi/helpers.ts | 276 +++++++++++++++ lib/openapi/index.ts | 3 + lib/openapi/loader.ts | 36 ++ lib/openapi/types.ts | 46 +++ next.config.js | 22 +- 21 files changed, 2128 insertions(+), 20 deletions(-) create mode 100644 app/(api-reference)/api-reference/[[...slug]]/loading.tsx create mode 100644 app/(api-reference)/api-reference/[[...slug]]/page.tsx create mode 100644 app/(api-reference)/api-reference/components/expandable-response.tsx create mode 100644 app/(api-reference)/api-reference/components/index.ts create mode 100644 app/(api-reference)/api-reference/components/loading-skeleton.tsx create mode 100644 app/(api-reference)/api-reference/components/method-content.tsx create mode 100644 app/(api-reference)/api-reference/components/multi-lang-example-server.tsx create mode 100644 app/(api-reference)/api-reference/components/operation-parameters-server.tsx create mode 100644 app/(api-reference)/api-reference/components/page-shell.tsx create mode 100644 app/(api-reference)/api-reference/components/pre-content.tsx create mode 100644 app/(api-reference)/api-reference/components/resource-section.tsx create mode 100644 app/(api-reference)/api-reference/components/schema-properties-server.tsx create mode 100644 app/(api-reference)/layout.tsx create mode 100644 app/(api-reference)/mapi-reference/[[...slug]]/loading.tsx create mode 100644 app/(api-reference)/mapi-reference/[[...slug]]/page.tsx create mode 100644 app/(api-reference)/providers.tsx create mode 100644 lib/openapi/helpers.ts create mode 100644 lib/openapi/index.ts create mode 100644 lib/openapi/loader.ts create mode 100644 lib/openapi/types.ts diff --git a/app/(api-reference)/api-reference/[[...slug]]/loading.tsx b/app/(api-reference)/api-reference/[[...slug]]/loading.tsx new file mode 100644 index 000000000..951254c75 --- /dev/null +++ b/app/(api-reference)/api-reference/[[...slug]]/loading.tsx @@ -0,0 +1,36 @@ +import { Box, Stack } from "@telegraph/layout"; + +export default function Loading() { + return ( + + + + + + {[1, 2, 3].map((i) => ( + + ))} + + + + ); +} diff --git a/app/(api-reference)/api-reference/[[...slug]]/page.tsx b/app/(api-reference)/api-reference/[[...slug]]/page.tsx new file mode 100644 index 000000000..077b829a8 --- /dev/null +++ b/app/(api-reference)/api-reference/[[...slug]]/page.tsx @@ -0,0 +1,133 @@ +import { Suspense } from "react"; +import { getStainlessSpec } from "../../../../lib/openapi/loader"; +import { buildSchemaReferences, getSidebarContent } from "../../../../lib/openapi/helpers"; +import { RESOURCE_ORDER, API_REFERENCE_OVERVIEW_CONTENT } from "../../../../data/sidebars/apiOverviewSidebar"; +import { PageShell, ResourceSection, PreContent, ResourceSectionSkeleton } from "../components"; +import type { Metadata } from "next"; + +export const dynamic = "force-static"; +export const revalidate = 3600; + +export async function generateMetadata(): Promise { + return { + title: "API reference | Knock Docs", + description: "Complete reference documentation for the Knock API.", + alternates: { + canonical: "/api-reference", + }, + }; +} + +// Generate all possible static paths +export async function generateStaticParams() { + const stainless = await getStainlessSpec("api"); + const paths: { slug: string[] }[] = []; + + // Root path (no slug) + paths.push({ slug: [] }); + + // Overview paths + API_REFERENCE_OVERVIEW_CONTENT.forEach((section) => { + if (section.pages) { + section.pages.forEach((page) => { + const slug = page.slug.replace(/^\//, "").split("/").filter(Boolean); + if (slug.length > 0) { + paths.push({ slug: ["overview", ...slug] }); + } + }); + } + }); + paths.push({ slug: ["overview"] }); + + // Resource paths + function addResourcePaths( + resourceName: string, + resource: (typeof stainless.resources)[string], + basePath: string[] = [], + ) { + const resourcePath = [...basePath, resourceName]; + paths.push({ slug: resourcePath }); + + // Method paths + if (resource.methods) { + Object.keys(resource.methods).forEach((methodName) => { + paths.push({ slug: [...resourcePath, methodName] }); + }); + } + + // Schema paths + if (resource.models) { + paths.push({ slug: [...resourcePath, "schemas"] }); + Object.keys(resource.models).forEach((modelName) => { + paths.push({ slug: [...resourcePath, "schemas", modelName] }); + }); + } + + // Subresource paths + if (resource.subresources) { + Object.entries(resource.subresources).forEach(([subName, subResource]) => { + addResourcePaths(subName, subResource, resourcePath); + }); + } + } + + RESOURCE_ORDER.forEach((resourceName) => { + const resource = stainless.resources[resourceName]; + if (resource) { + addResourcePaths(resourceName, resource); + } + }); + + return paths; +} + +export default async function ApiReferencePage() { + const stainless = await getStainlessSpec("api"); + const schemaReferences = await buildSchemaReferences("api", "/api-reference"); + const baseUrl = stainless.environments.production; + + // Transform sidebar content for compatibility + const sidebarContent = await getSidebarContent( + "api", + RESOURCE_ORDER, + "/api-reference", + API_REFERENCE_OVERVIEW_CONTENT.map((section) => ({ + ...section, + title: section.title || "", + })), + ); + + // Transform to SidebarSection format + const transformedSidebar = sidebarContent.map((section) => ({ + title: section.title, + slug: section.slug, + pages: section.pages || [], + sidebarMenuDefaultOpen: + API_REFERENCE_OVERVIEW_CONTENT.find((s) => s.slug === section.slug) + ?.sidebarMenuDefaultOpen ?? false, + })); + + return ( + + }> + + + + {RESOURCE_ORDER.map((resourceName) => ( + }> + + + ))} + + ); +} diff --git a/app/(api-reference)/api-reference/components/expandable-response.tsx b/app/(api-reference)/api-reference/components/expandable-response.tsx new file mode 100644 index 000000000..2f503ffc6 --- /dev/null +++ b/app/(api-reference)/api-reference/components/expandable-response.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { useState } from "react"; +import type { OpenAPIV3 } from "@scalar/openapi-types"; +import { AnimatePresence, motion } from "framer-motion"; +import Link from "next/link"; +import { Box, Stack } from "@telegraph/layout"; +import { Text, Code } from "@telegraph/typography"; +import { ChevronDown, ChevronRight } from "lucide-react"; + +interface ExpandableResponseProps { + responseSchema: OpenAPIV3.SchemaObject; + schemaReferences: Record; +} + +export function ExpandableResponse({ + responseSchema, + schemaReferences, +}: ExpandableResponseProps) { + const [isExpanded, setIsExpanded] = useState(false); + const schemaRef = schemaReferences[responseSchema.title ?? ""]; + + return ( + + + + {schemaRef ? ( + + {responseSchema.title} + + ) : ( + + {responseSchema.title} + + )} + + {responseSchema.description && ( + + {responseSchema.description} + + )} + + {responseSchema.properties && ( + <> + + + + {isExpanded && ( + + + + + + )} + + + )} + + + ); +} + +// Inline client version of schema properties for nested rendering +function SchemaPropertiesClient({ + schema, + schemaReferences, + hideRequired = false, +}: { + schema: OpenAPIV3.SchemaObject; + schemaReferences: Record; + hideRequired?: boolean; +}) { + return ( + + {Object.entries(schema.properties || {}).map( + ([propertyName, property]) => { + const prop = property as OpenAPIV3.SchemaObject; + const isRequired = !hideRequired && schema.required?.includes(propertyName); + const typeString = getTypeString(prop); + const typeRef = schemaReferences[typeString]; + + return ( + + + + {propertyName} + + {typeRef ? ( + + {typeString} + + ) : ( + + {typeString} + + )} + {isRequired && ( + + required + + )} + + {prop.description && ( + + {prop.description} + + )} + {prop.enum && ( + + One of: {prop.enum.map((e) => `"${e}"`).join(", ")} + + )} + + ); + }, + )} + + ); +} + +function getTypeString(schema: OpenAPIV3.SchemaObject): string { + if (schema.title) return schema.title; + if (schema.type === "array" && schema.items) { + const items = schema.items as OpenAPIV3.SchemaObject; + return `${items.title || items.type || "unknown"}[]`; + } + return schema.type || "unknown"; +} diff --git a/app/(api-reference)/api-reference/components/index.ts b/app/(api-reference)/api-reference/components/index.ts new file mode 100644 index 000000000..a88517533 --- /dev/null +++ b/app/(api-reference)/api-reference/components/index.ts @@ -0,0 +1,9 @@ +export { PageShell } from "./page-shell"; +export { ResourceSection } from "./resource-section"; +export { MethodContent } from "./method-content"; +export { SchemaPropertiesServer } from "./schema-properties-server"; +export { OperationParametersServer } from "./operation-parameters-server"; +export { MultiLangExampleServer } from "./multi-lang-example-server"; +export { ExpandableResponse } from "./expandable-response"; +export { PreContent } from "./pre-content"; +export { ResourceSectionSkeleton } from "./loading-skeleton"; diff --git a/app/(api-reference)/api-reference/components/loading-skeleton.tsx b/app/(api-reference)/api-reference/components/loading-skeleton.tsx new file mode 100644 index 000000000..99990dbeb --- /dev/null +++ b/app/(api-reference)/api-reference/components/loading-skeleton.tsx @@ -0,0 +1,47 @@ +import { Box, Stack } from "@telegraph/layout"; + +export function ResourceSectionSkeleton() { + return ( + + + + + + + + + + + + ); +} diff --git a/app/(api-reference)/api-reference/components/method-content.tsx b/app/(api-reference)/api-reference/components/method-content.tsx new file mode 100644 index 000000000..a11e89851 --- /dev/null +++ b/app/(api-reference)/api-reference/components/method-content.tsx @@ -0,0 +1,254 @@ +import type { OpenAPIV3 } from "@scalar/openapi-types"; +import Markdown from "react-markdown"; +import { Box } from "@telegraph/layout"; +import { Code, Heading } from "@telegraph/typography"; +import { Callout } from "../../../../components/ui/Callout"; +import RateLimit from "../../../../components/ui/RateLimit"; +import { ContentColumn, ExampleColumn, Section } from "../../../../components/ui/ApiSections"; +import { CodeBlock } from "../../../../components/ui/CodeBlock"; +import { Endpoint } from "../../../../components/ui/Endpoints"; +import { getOpenApiSpec } from "../../../../lib/openapi/loader"; +import { + getOperation, + augmentSnippetsWithCurlRequest, + formatResponseStatusCodes, + resolveResponseSchemas, +} from "../../../../lib/openapi/helpers"; +import type { SpecName } from "../../../../lib/openapi/types"; +import { OperationParametersServer } from "./operation-parameters-server"; +import { SchemaPropertiesServer } from "./schema-properties-server"; +import { ExpandableResponse } from "./expandable-response"; +import { MultiLangExampleServer } from "./multi-lang-example-server"; + +interface MethodContentProps { + specName: SpecName; + methodName: string; + methodType: "get" | "post" | "put" | "delete"; + endpoint: string; + path: string; + mdPath: string; + baseUrl: string; + schemaReferences: Record; +} + +export async function MethodContent({ + specName, + methodName, + methodType, + endpoint, + path, + mdPath, + baseUrl, + schemaReferences, +}: MethodContentProps) { + const openApiSpec = await getOpenApiSpec(specName); + const method = getOperation(openApiSpec, methodType, endpoint); + + if (!method) { + return null; + } + + const parameters = (method.parameters || []) as OpenAPIV3.ParameterObject[]; + const pathParameters = parameters.filter((p) => p.in === "path"); + const queryParameters = parameters.filter((p) => p.in === "query"); + + const responseSchemas = resolveResponseSchemas(method); + const requestBody = ( + method.requestBody as OpenAPIV3.RequestBodyObject | undefined + )?.content?.["application/json"]?.schema as + | OpenAPIV3.SchemaObject + | undefined; + + const rateLimit = (method as Record)?.["x-ratelimit-tier"] as + | number + | null; + const isIdempotent = + ((method as Record)?.["x-idempotent"] as boolean) ?? false; + const isRetentionSubject = + ((method as Record)?.["x-retention-policy"] as boolean) ?? + false; + const isBeta = + ((method as Record)?.["x-beta"] as boolean) ?? false; + + const snippets = (method as Record)?.[ + "x-stainless-snippets" + ] as Record | undefined; + + return ( +
+ + {method.description ?? ""} + {isBeta && ( + + This endpoint is currently in beta. If you'd like early access, + or this is blocking your adoption of Knock, please{" "} + + get in touch + + . + + } + /> + )} + + + Endpoint + + + + + {rateLimit && ( + + + Rate limit + + + + )} + + {pathParameters.length > 0 && ( + <> + + Path parameters + + + + )} + + {queryParameters.length > 0 && ( + <> + + Query parameters + + + + )} + + {requestBody && ( + <> + + Request body + + + + )} + + + Returns + + + {responseSchemas.length > 0 && + responseSchemas.map((responseSchema) => ( + + ))} + + {responseSchemas.length === 0 && ( + + {formatResponseStatusCodes(method).map((formattedStatus, index) => ( + + {formattedStatus} + + ))} + + )} + + + + {responseSchemas.map( + (responseSchema) => + responseSchema?.example && ( + + {JSON.stringify(responseSchema?.example, null, 2)} + + ), + )} + +
+ ); +} diff --git a/app/(api-reference)/api-reference/components/multi-lang-example-server.tsx b/app/(api-reference)/api-reference/components/multi-lang-example-server.tsx new file mode 100644 index 000000000..c8e461e7d --- /dev/null +++ b/app/(api-reference)/api-reference/components/multi-lang-example-server.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useEventEmitter } from "@byteclaw/use-event-emitter"; +import { useEffect, useMemo } from "react"; + +import { useIsMounted } from "../../../../hooks/useIsMounted"; +import useLocalStorage from "../../../../hooks/useLocalStorage"; +import { + CodeBlock, + SupportedLanguage, +} from "../../../../components/ui/CodeBlock"; +import { EVENT_NAME, LOCAL_STORAGE_KEY } from "../../../../components/ui/MultiLangCodeBlock"; + +interface MultiLangExampleServerProps { + examples: Record; + title: string; +} + +function resolveLanguageToSnippet(language: string) { + switch (language) { + case "node": + return "typescript"; + default: + return language; + } +} + +function resolveSnippetLanguages(language: string) { + switch (language) { + case "typescript": + return "node"; + default: + return language; + } +} + +export function MultiLangExampleServer({ + examples, + title, +}: MultiLangExampleServerProps) { + const isMounted = useIsMounted(); + const eventEmitter = useEventEmitter(); + + const languages = useMemo( + () => Object.keys(examples).map(resolveSnippetLanguages), + [examples], + ); + + const [language, setLanguage] = useLocalStorage( + LOCAL_STORAGE_KEY, + "node", + ); + + const resolvedLanguage = resolveLanguageToSnippet(language); + + useEffect(() => { + const unsubscribe = eventEmitter.on(EVENT_NAME, setLanguage); + return () => unsubscribe(); + }, [eventEmitter, setLanguage]); + + useEffect(() => { + eventEmitter.emit(EVENT_NAME, language); + }, [language, eventEmitter]); + + const exampleContent = useMemo(() => { + const exampleInSelectedLanguage = examples[resolvedLanguage]; + const listedLanguage = languages && languages[0]; + + if (!exampleInSelectedLanguage) { + return examples[listedLanguage]; + } + + return exampleInSelectedLanguage; + }, [examples, resolvedLanguage, languages]); + + if (!isMounted) return null; + + return ( + + {exampleContent} + + ); +} diff --git a/app/(api-reference)/api-reference/components/operation-parameters-server.tsx b/app/(api-reference)/api-reference/components/operation-parameters-server.tsx new file mode 100644 index 000000000..d6ef6559b --- /dev/null +++ b/app/(api-reference)/api-reference/components/operation-parameters-server.tsx @@ -0,0 +1,76 @@ +import type { OpenAPIV3 } from "@scalar/openapi-types"; +import Link from "next/link"; +import { Box, Stack } from "@telegraph/layout"; +import { Text, Code } from "@telegraph/typography"; + +interface OperationParametersServerProps { + parameters: OpenAPIV3.ParameterObject[]; + schemaReferences: Record; +} + +export function OperationParametersServer({ + parameters, + schemaReferences, +}: OperationParametersServerProps) { + return ( + + {parameters.map((parameter) => { + const schema = parameter.schema as OpenAPIV3.SchemaObject | undefined; + const typeString = getTypeString(schema); + const typeRef = schemaReferences[typeString]; + + return ( + + + + {parameter.name} + + {typeRef ? ( + + {typeString} + + ) : ( + + {typeString} + + )} + {parameter.required && ( + + required + + )} + + {parameter.description && ( + + {parameter.description} + + )} + {schema?.enum && ( + + One of: {schema.enum.map((e) => `"${e}"`).join(", ")} + + )} + {schema?.default !== undefined && ( + + Default: {JSON.stringify(schema.default)} + + )} + + ); + })} + + ); +} + +function getTypeString(schema?: OpenAPIV3.SchemaObject): string { + if (!schema) return "unknown"; + if (schema.title) return schema.title; + if (schema.type === "array" && schema.items) { + const items = schema.items as OpenAPIV3.SchemaObject; + return `${items.title || items.type || "unknown"}[]`; + } + return schema.type || "unknown"; +} diff --git a/app/(api-reference)/api-reference/components/page-shell.tsx b/app/(api-reference)/api-reference/components/page-shell.tsx new file mode 100644 index 000000000..96b3200e1 --- /dev/null +++ b/app/(api-reference)/api-reference/components/page-shell.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useRef, useContext } from "react"; +import { Box, Stack } from "@telegraph/layout"; +import { Heading, Text } from "@telegraph/typography"; +import { PageHeader } from "../../../../components/ui/PageHeader"; +import { MobileSidebar } from "../../../../components/ui/Page/MobileSidebar"; +import { Sidebar, SidebarContext } from "../../../../components/ui/Page/Sidebar"; +import { ContentActions } from "../../../../components/ui/ContentActions"; +import { AskAiContext } from "../../../../components/AskAiContext"; +import type { SidebarSection } from "../../../../data/types"; + +interface PageShellProps { + children: React.ReactNode; + sidebarContent: SidebarSection[]; + title: string; + description: string; +} + +export function PageShell({ + children, + sidebarContent, + title, + description, +}: PageShellProps) { + const scrollerRef = useRef(null); + const askAiContext = useContext(AskAiContext); + const isOpen = askAiContext?.isOpen ?? false; + const sidebarWidth = askAiContext?.sidebarWidth ?? 340; + const isResizing = askAiContext?.isResizing ?? false; + + return ( + + + + {sidebarContent.map((section) => ( + + ))} + + + } + /> +
+ + + + {sidebarContent.map((section) => ( + + ))} + + + + +
+
+ + + {title} + + + {description} + + + + + + + {children} + +
+
+
+
+ ); +} diff --git a/app/(api-reference)/api-reference/components/pre-content.tsx b/app/(api-reference)/api-reference/components/pre-content.tsx new file mode 100644 index 000000000..eeb26d70d --- /dev/null +++ b/app/(api-reference)/api-reference/components/pre-content.tsx @@ -0,0 +1,57 @@ +import { readFile } from "fs/promises"; +import { MDXRemote } from "next-mdx-remote/rsc"; +import remarkGfm from "remark-gfm"; +import rehypeMdxCodeProps from "rehype-mdx-code-props"; +import { CONTENT_DIR } from "../../../../lib/content.server"; +import { Section, ContentColumn, ExampleColumn } from "../../../../components/ui/ApiSections"; +import { CodeBlock } from "../../../../components/ui/CodeBlock"; +import { Callout } from "../../../../components/ui/Callout"; +import { Endpoints, Endpoint } from "../../../../components/ui/Endpoints"; +import { Table } from "../../../../components/ui/Table"; +import RateLimit from "../../../../components/ui/RateLimit"; +import { Attributes, Attribute } from "../../../../components/ui/Attributes"; +import { ErrorExample } from "../../../../components/ui/ApiSections"; +import MultiLangCodeBlock from "../../../../components/ui/MultiLangCodeBlock"; +import type { SpecName } from "../../../../lib/openapi/types"; + +const MDX_COMPONENTS = { + Section, + ContentColumn, + ExampleColumn, + CodeBlock, + Callout, + Endpoints, + Endpoint, + Table, + RateLimit, + Attributes, + Attribute, + ErrorExample, + MultiLangCodeBlock, +}; + +interface PreContentProps { + specName: SpecName; +} + +export async function PreContent({ specName }: PreContentProps) { + const contentPath = + specName === "api" + ? `${CONTENT_DIR}/__api-reference/content.mdx` + : `${CONTENT_DIR}/__mapi-reference/content.mdx`; + + const content = await readFile(contentPath, "utf8"); + + return ( + + ); +} diff --git a/app/(api-reference)/api-reference/components/resource-section.tsx b/app/(api-reference)/api-reference/components/resource-section.tsx new file mode 100644 index 000000000..5867dc8a9 --- /dev/null +++ b/app/(api-reference)/api-reference/components/resource-section.tsx @@ -0,0 +1,324 @@ +import React from "react"; +import type { OpenAPIV3 } from "@scalar/openapi-types"; +import Markdown from "react-markdown"; +import { Box } from "@telegraph/layout"; +import { Heading } from "@telegraph/typography"; +import { CodeBlock } from "../../../../components/ui/CodeBlock"; +import { Endpoint, Endpoints } from "../../../../components/ui/Endpoints"; +import { ContentColumn, ExampleColumn, Section } from "../../../../components/ui/ApiSections"; +import { getOpenApiSpec, getStainlessSpec } from "../../../../lib/openapi/loader"; +import { + resolveEndpoint, + getOperation, + getSchemaByRef, + getResourceMethods, + getResourceSchemas, + buildSchemaReferences, +} from "../../../../lib/openapi/helpers"; +import type { SpecName, StainlessResource } from "../../../../lib/openapi/types"; +import { MethodContent } from "./method-content"; +import { SchemaPropertiesServer } from "./schema-properties-server"; + +interface ResourceSectionProps { + specName: SpecName; + resourceName: string; + basePath: string; + baseUrl: string; + schemaReferences: Record; + path?: string; +} + +export async function ResourceSection({ + specName, + resourceName, + basePath, + baseUrl, + schemaReferences, + path, +}: ResourceSectionProps) { + const [openApiSpec, stainlessSpec] = await Promise.all([ + getOpenApiSpec(specName), + getStainlessSpec(specName), + ]); + + const resource = stainlessSpec.resources[resourceName]; + if (!resource) { + return null; + } + + const methods = resource.methods || {}; + const models = resource.models || {}; + const resourcePath = path ?? `/${resourceName}`; + const apiSurface = basePath.replace("/", ""); + + return ( + <> + +
+ + {resource.description && ( + {resource.description} + )} + + + {Object.entries(methods).length > 0 && ( + + {Object.entries(methods).map( + ([methodName, endpointOrMethodConfig]) => { + const [methodType, endpoint] = resolveEndpoint( + endpointOrMethodConfig, + ); + + return ( + + ); + }, + )} + + )} + +
+
+ + {Object.entries(methods).map(([methodName, endpointOrMethodConfig]) => { + const [methodType, endpoint] = resolveEndpoint(endpointOrMethodConfig); + const methodPath = `${resourcePath}/${methodName}`; + const methodMdPath = `/${apiSurface}${resourcePath}/${methodName}.md`; + + return ( + + + + ); + })} + + {Object.entries(resource.subresources ?? {}).map( + ([subresourceName, subresource]) => { + return ( + + ); + }, + )} + + {Object.entries(models).map(([modelName, modelReference]) => { + const schema = getSchemaByRef(openApiSpec, modelReference); + + if (!schema) { + return null; + } + + const schemaPath = `${resourcePath}/schemas/${modelName}`; + const schemaMdPath = `/${apiSurface}${resourcePath}/schemas/${modelName}.md`; + + return ( + +
+ + {schema.description && ( + {schema.description} + )} + + + Attributes + + + + + + {JSON.stringify(schema.example, null, 2)} + + +
+
+ ); + })} + + ); +} + +interface SubResourceSectionProps { + specName: SpecName; + resource: StainlessResource; + resourceName: string; + basePath: string; + baseUrl: string; + schemaReferences: Record; + path: string; +} + +async function SubResourceSection({ + specName, + resource, + resourceName, + basePath, + baseUrl, + schemaReferences, + path, +}: SubResourceSectionProps) { + const openApiSpec = await getOpenApiSpec(specName); + + const methods = resource.methods || {}; + const models = resource.models || {}; + const apiSurface = basePath.replace("/", ""); + + return ( + <> + +
+ + {resource.description && ( + {resource.description} + )} + + + {Object.entries(methods).length > 0 && ( + + {Object.entries(methods).map( + ([methodName, endpointOrMethodConfig]) => { + const [methodType, endpoint] = resolveEndpoint( + endpointOrMethodConfig, + ); + + return ( + + ); + }, + )} + + )} + +
+
+ + {Object.entries(methods).map(([methodName, endpointOrMethodConfig]) => { + const [methodType, endpoint] = resolveEndpoint(endpointOrMethodConfig); + const methodPath = `${path}/${methodName}`; + const methodMdPath = `/${apiSurface}${path}/${methodName}.md`; + + return ( + + + + ); + })} + + {Object.entries(models).map(([modelName, modelReference]) => { + const schema = getSchemaByRef(openApiSpec, modelReference); + + if (!schema) { + return null; + } + + const schemaPath = `${path}/schemas/${modelName}`; + const schemaMdPath = `/${apiSurface}${path}/schemas/${modelName}.md`; + + return ( + +
+ + {schema.description && ( + {schema.description} + )} + + + Attributes + + + + + + {JSON.stringify(schema.example, null, 2)} + + +
+
+ ); + })} + + ); +} diff --git a/app/(api-reference)/api-reference/components/schema-properties-server.tsx b/app/(api-reference)/api-reference/components/schema-properties-server.tsx new file mode 100644 index 000000000..19b793b2c --- /dev/null +++ b/app/(api-reference)/api-reference/components/schema-properties-server.tsx @@ -0,0 +1,207 @@ +import type { OpenAPIV3 } from "@scalar/openapi-types"; +import Link from "next/link"; +import { Box, Stack } from "@telegraph/layout"; +import { Text, Code } from "@telegraph/typography"; + +interface SchemaPropertiesServerProps { + schema: OpenAPIV3.SchemaObject; + schemaReferences: Record; + hideRequired?: boolean; +} + +export function SchemaPropertiesServer({ + schema, + schemaReferences, + hideRequired = false, +}: SchemaPropertiesServerProps) { + const unionSchema = schema.oneOf || schema.anyOf || schema.allOf; + const onlyUnion = + unionSchema && !schema.properties && !schema.additionalProperties; + + return ( + + {Object.entries(schema.properties || {}).map( + ([propertyName, property]) => ( + + ), + )} + {schema.additionalProperties && ( + + )} + + {onlyUnion && ( + + )} + + ); +} + +interface SchemaPropertyServerProps { + name?: string; + schema: OpenAPIV3.SchemaObject & { required?: boolean | string[] }; + schemaReferences: Record; +} + +function SchemaPropertyServer({ + name, + schema, + schemaReferences, +}: SchemaPropertyServerProps) { + const typeString = getTypeString(schema); + const typeRef = schemaReferences[typeString]; + const isRequired = + schema.required === true || + (Array.isArray(schema.required) && name && schema.required.includes(name)); + const isNullable = schema.nullable; + + // Handle union types (oneOf, anyOf, allOf) + const unionSchema = schema.oneOf || schema.anyOf || schema.allOf; + + return ( + + + {name && ( + + {name} + + )} + {typeRef ? ( + + {typeString} + + ) : ( + + {typeString} + + )} + {isRequired && ( + + required + + )} + {isNullable && ( + + nullable + + )} + + {schema.description && ( + + {schema.description} + + )} + {schema.enum && ( + + One of: {schema.enum.map((e) => `"${e}"`).join(", ")} + + )} + {schema.default !== undefined && ( + + Default: {JSON.stringify(schema.default)} + + )} + + {/* Handle nested properties */} + {schema.properties && ( + + + + )} + + {/* Handle array items with properties */} + {schema.type === "array" && + schema.items && + (schema.items as OpenAPIV3.SchemaObject).properties && ( + + + + )} + + {/* Handle union types */} + {unionSchema && ( + + + {schema.oneOf + ? "One of:" + : schema.anyOf + ? "Any of:" + : "All of:"} + + + {unionSchema.map((unionItem, index) => { + const item = unionItem as OpenAPIV3.SchemaObject; + const itemTypeString = getTypeString(item); + const itemTypeRef = schemaReferences[itemTypeString]; + + return ( + + {itemTypeRef ? ( + + {itemTypeString} + + ) : ( + + {itemTypeString} + + )} + {item.description && ( + + {item.description} + + )} + + ); + })} + + + )} + + ); +} + +function getTypeString(schema: OpenAPIV3.SchemaObject): string { + if (schema.title) return schema.title; + if (schema.type === "array" && schema.items) { + const items = schema.items as OpenAPIV3.SchemaObject; + return `${items.title || items.type || "unknown"}[]`; + } + if (schema.oneOf) return "oneOf"; + if (schema.anyOf) return "anyOf"; + if (schema.allOf) return "allOf"; + return schema.type || "unknown"; +} diff --git a/app/(api-reference)/layout.tsx b/app/(api-reference)/layout.tsx new file mode 100644 index 000000000..331a00ddc --- /dev/null +++ b/app/(api-reference)/layout.tsx @@ -0,0 +1,49 @@ +import { Inter } from "next/font/google"; +import { Providers } from "./providers"; +import { GA_TRACKING_ID } from "../../lib/gtag"; + +import "@algolia/autocomplete-theme-classic"; +import "../../styles/index.css"; +import "../../styles/global.css"; +import "../../styles/responsive.css"; + +const inter = Inter({ subsets: ["latin"], display: "swap" }); + +export default function ApiReferenceLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +