From c87889f177d76ccc0b1c16f1a377866ff42fb1cf Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 17 Oct 2024 18:15:07 -0400 Subject: [PATCH 1/3] fix: api definition endpoint --- packages/ui/app/src/hooks/useApiRouteSWR.ts | 26 ++++++- .../playground/hooks/useEndpointContext.ts | 11 ++- .../playground/hooks/useWebSocketContext.ts | 11 ++- .../src/pages/api/fern-docs/api-definition.ts | 75 +++++++++++++++++++ 4 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts diff --git a/packages/ui/app/src/hooks/useApiRouteSWR.ts b/packages/ui/app/src/hooks/useApiRouteSWR.ts index 0ac843a485..0eead7d9a0 100644 --- a/packages/ui/app/src/hooks/useApiRouteSWR.ts +++ b/packages/ui/app/src/hooks/useApiRouteSWR.ts @@ -1,3 +1,4 @@ +import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; import useSWR, { Fetcher, SWRConfiguration, SWRResponse } from "swr"; import useSWRImmutable from "swr/immutable"; import { withSkewProtection } from "../util/withSkewProtection"; @@ -23,5 +24,28 @@ export function useApiRouteSWR(route: FernDocsApiRoute, options?: Options) export function useApiRouteSWRImmutable(route: FernDocsApiRoute, options?: Options): SWRResponse { const key = useApiRoute(route); - return useSWRImmutable(options?.disabled ? null : key, createFetcher(options?.request), options); + return useSWRImmutable([options?.disabled ? null : key], createFetcher(options?.request), options); +} + +// TODO: this fetcher is a little bit precarious, since it's not type-safe and was created hastily to resolve a specific issue. should be refactored. +// context: sometimes forward proxies will decode %2F to /, which will cause the api endpoint to not match the correct route. +// in that case, we needed to change the "GET" request to a "POST" request to get the correct endpoint. +export function useApiDefinitionSWR( + api: string | undefined, + endpointId: string | undefined, + type: "endpoint" | "websocket" | "webhook", + options?: Options, +): SWRResponse { + const route = useApiRoute("/api/fern-docs/api-definition"); + return useSWRImmutable( + options?.disabled || api == null || endpointId == null ? null : [route, api, endpointId, type], + (): Promise => { + return createFetcher({ + ...options?.request, + method: "POST", + body: JSON.stringify({ api, [type]: endpointId }), + })(route); + }, + options, + ); } diff --git a/packages/ui/app/src/playground/hooks/useEndpointContext.ts b/packages/ui/app/src/playground/hooks/useEndpointContext.ts index 0835eb14f7..b55a4d0ec2 100644 --- a/packages/ui/app/src/playground/hooks/useEndpointContext.ts +++ b/packages/ui/app/src/playground/hooks/useEndpointContext.ts @@ -1,8 +1,8 @@ -import { createEndpointContext, type ApiDefinition, type EndpointContext } from "@fern-api/fdr-sdk/api-definition"; +import { createEndpointContext, type EndpointContext } from "@fern-api/fdr-sdk/api-definition"; import type * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { useMemo } from "react"; import { useWriteApiDefinitionAtom } from "../../atoms"; -import { useApiRouteSWRImmutable } from "../../hooks/useApiRouteSWR"; +import { useApiDefinitionSWR } from "../../hooks/useApiRouteSWR"; interface LoadableEndpointContext { context: EndpointContext | undefined; @@ -14,10 +14,9 @@ interface LoadableEndpointContext { * It should be refactored to store the resulting endpoint in a global state, so that it can be shared between components. */ export function useEndpointContext(node: FernNavigation.EndpointNode | undefined): LoadableEndpointContext { - const { data: apiDefinition, isLoading } = useApiRouteSWRImmutable( - `/api/fern-docs/api-definition/${encodeURIComponent(node?.apiDefinitionId ?? "")}/endpoint/${encodeURIComponent(node?.endpointId ?? "")}`, - { disabled: node == null }, - ); + const { data: apiDefinition, isLoading } = useApiDefinitionSWR(node?.apiDefinitionId, node?.id, "endpoint", { + disabled: node == null, + }); const context = useMemo(() => createEndpointContext(node, apiDefinition), [node, apiDefinition]); useWriteApiDefinitionAtom(apiDefinition); diff --git a/packages/ui/app/src/playground/hooks/useWebSocketContext.ts b/packages/ui/app/src/playground/hooks/useWebSocketContext.ts index 05e86458b8..7db4f82b83 100644 --- a/packages/ui/app/src/playground/hooks/useWebSocketContext.ts +++ b/packages/ui/app/src/playground/hooks/useWebSocketContext.ts @@ -1,10 +1,10 @@ -import type { ApiDefinition, WebSocketContext } from "@fern-api/fdr-sdk/api-definition"; +import type { WebSocketContext } from "@fern-api/fdr-sdk/api-definition"; import { createWebSocketContext } from "@fern-api/fdr-sdk/api-definition"; import type * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { useSetAtom } from "jotai"; import { useEffect, useMemo } from "react"; import { WRITE_API_DEFINITION_ATOM } from "../../atoms"; -import { useApiRouteSWRImmutable } from "../../hooks/useApiRouteSWR"; +import { useApiDefinitionSWR } from "../../hooks/useApiRouteSWR"; interface LoadableWebSocketContext { context: WebSocketContext | undefined; @@ -16,10 +16,9 @@ interface LoadableWebSocketContext { * It should be refactored to store the resulting endpoint in a global state, so that it can be shared between components. */ export function useWebSocketContext(node: FernNavigation.WebSocketNode): LoadableWebSocketContext { - const { data: apiDefinition, isLoading } = useApiRouteSWRImmutable( - `/api/fern-docs/api-definition/${encodeURIComponent(node.apiDefinitionId)}/websocket/${encodeURIComponent(node.webSocketId)}`, - { disabled: node == null }, - ); + const { data: apiDefinition, isLoading } = useApiDefinitionSWR(node.apiDefinitionId, node.id, "websocket", { + disabled: node == null, + }); const context = useMemo(() => createWebSocketContext(node, apiDefinition), [node, apiDefinition]); const set = useSetAtom(WRITE_API_DEFINITION_ATOM); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts new file mode 100644 index 0000000000..f7e2515a90 --- /dev/null +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts @@ -0,0 +1,75 @@ +import { getDocsDomainNode } from "@/server/xfernhost/node"; +import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; +import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config"; +import { ApiDefinitionLoader } from "@fern-ui/fern-docs-server"; +import { getMdxBundler } from "@fern-ui/ui/bundlers"; +import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +const schema = z.object({ + api: z.string(), + endpoint: z.string().optional(), + websocket: z.string().optional(), + webhook: z.string().optional(), +}); + +const resolveApiHandler: NextApiHandler = async ( + req: NextApiRequest, + res: NextApiResponse, +) => { + const xFernHost = getDocsDomainNode(req); + if (req.method !== "POST") { + res.status(400).end(); + return; + } + + const body = schema.safeParse(req.body); + + if (!body.success) { + // eslint-disable-next-line no-console + console.error(body.error); + res.status(400).end(); + return; + } + + const { api, endpoint, websocket, webhook } = body.data; + + const flags = await getFeatureFlags(xFernHost); + + // TODO: pass in other tsx/mdx files to serializeMdx options + const engine = flags.useMdxBundler ? "mdx-bundler" : "next-mdx-remote"; + const serializeMdx = await getMdxBundler(engine); + + // TODO: authenticate the request in FDR + const loader = ApiDefinitionLoader.create(xFernHost, ApiDefinition.ApiDefinitionId(api)) + .withFlags(flags) + .withMdxBundler(serializeMdx, engine) + // .withPrune({ type: "endpoint", endpointId: ApiDefinition.EndpointId(endpoint) }) + .withResolveDescriptions() + .withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN); + + if (endpoint != null) { + loader.withPrune({ type: "endpoint", endpointId: ApiDefinition.EndpointId(endpoint) }); + } + + if (websocket != null) { + loader.withPrune({ type: "webSocket", webSocketId: ApiDefinition.WebSocketId(websocket) }); + } + + if (webhook != null) { + loader.withPrune({ type: "webhook", webhookId: ApiDefinition.WebhookId(webhook) }); + } + + const apiDefinition = await loader.load(); + + if (!apiDefinition) { + return res.status(404).end(); + } + + // Cache the response in Vercel's Data Cache for 1 hour, and allow it to be served stale for up to 24 hours + res.setHeader("Cache-Control", "public, s-maxage=3600, stale-while-revalidate=86400"); + + return res.status(200).json(apiDefinition); +}; + +export default resolveApiHandler; From 0546da59d9ab3488f2c921e5e6762c002b508cec Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 17 Oct 2024 19:39:00 -0400 Subject: [PATCH 2/3] Fix --- .../ui/app/src/playground/hooks/useEndpointContext.ts | 11 ++++++++--- .../app/src/playground/hooks/useWebSocketContext.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/ui/app/src/playground/hooks/useEndpointContext.ts b/packages/ui/app/src/playground/hooks/useEndpointContext.ts index b55a4d0ec2..01d9e66b1f 100644 --- a/packages/ui/app/src/playground/hooks/useEndpointContext.ts +++ b/packages/ui/app/src/playground/hooks/useEndpointContext.ts @@ -14,9 +14,14 @@ interface LoadableEndpointContext { * It should be refactored to store the resulting endpoint in a global state, so that it can be shared between components. */ export function useEndpointContext(node: FernNavigation.EndpointNode | undefined): LoadableEndpointContext { - const { data: apiDefinition, isLoading } = useApiDefinitionSWR(node?.apiDefinitionId, node?.id, "endpoint", { - disabled: node == null, - }); + const { data: apiDefinition, isLoading } = useApiDefinitionSWR( + node?.apiDefinitionId, + node?.endpointId, + "endpoint", + { + disabled: node == null, + }, + ); const context = useMemo(() => createEndpointContext(node, apiDefinition), [node, apiDefinition]); useWriteApiDefinitionAtom(apiDefinition); diff --git a/packages/ui/app/src/playground/hooks/useWebSocketContext.ts b/packages/ui/app/src/playground/hooks/useWebSocketContext.ts index 7db4f82b83..8a2bae7367 100644 --- a/packages/ui/app/src/playground/hooks/useWebSocketContext.ts +++ b/packages/ui/app/src/playground/hooks/useWebSocketContext.ts @@ -16,9 +16,14 @@ interface LoadableWebSocketContext { * It should be refactored to store the resulting endpoint in a global state, so that it can be shared between components. */ export function useWebSocketContext(node: FernNavigation.WebSocketNode): LoadableWebSocketContext { - const { data: apiDefinition, isLoading } = useApiDefinitionSWR(node.apiDefinitionId, node.id, "websocket", { - disabled: node == null, - }); + const { data: apiDefinition, isLoading } = useApiDefinitionSWR( + node.apiDefinitionId, + node.webSocketId, + "websocket", + { + disabled: node == null, + }, + ); const context = useMemo(() => createWebSocketContext(node, apiDefinition), [node, apiDefinition]); const set = useSetAtom(WRITE_API_DEFINITION_ATOM); From 14ef0d0a2211f6bd0c23b6c126fbbda641100a75 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 17 Oct 2024 19:48:03 -0400 Subject: [PATCH 3/3] use query parameters --- packages/ui/app/src/hooks/useApiRouteSWR.ts | 14 ++++++-------- .../app/src/playground/hooks/useEndpointContext.ts | 4 +--- .../app/src/playground/hooks/usePreloadApiLeaf.ts | 8 ++++---- .../src/pages/api/fern-docs/api-definition.ts | 13 ++----------- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/packages/ui/app/src/hooks/useApiRouteSWR.ts b/packages/ui/app/src/hooks/useApiRouteSWR.ts index 0eead7d9a0..cc57927531 100644 --- a/packages/ui/app/src/hooks/useApiRouteSWR.ts +++ b/packages/ui/app/src/hooks/useApiRouteSWR.ts @@ -37,15 +37,13 @@ export function useApiDefinitionSWR( options?: Options, ): SWRResponse { const route = useApiRoute("/api/fern-docs/api-definition"); + const searchParams = new URLSearchParams(); + searchParams.set("api", api ?? ""); + searchParams.set(type, endpointId ?? ""); + const url = `${route}?${searchParams.toString()}`; return useSWRImmutable( - options?.disabled || api == null || endpointId == null ? null : [route, api, endpointId, type], - (): Promise => { - return createFetcher({ - ...options?.request, - method: "POST", - body: JSON.stringify({ api, [type]: endpointId }), - })(route); - }, + options?.disabled || api == null || endpointId == null ? null : url, + createFetcher(options?.request), options, ); } diff --git a/packages/ui/app/src/playground/hooks/useEndpointContext.ts b/packages/ui/app/src/playground/hooks/useEndpointContext.ts index 01d9e66b1f..54ecd566ab 100644 --- a/packages/ui/app/src/playground/hooks/useEndpointContext.ts +++ b/packages/ui/app/src/playground/hooks/useEndpointContext.ts @@ -18,9 +18,7 @@ export function useEndpointContext(node: FernNavigation.EndpointNode | undefined node?.apiDefinitionId, node?.endpointId, "endpoint", - { - disabled: node == null, - }, + { disabled: node == null }, ); const context = useMemo(() => createEndpointContext(node, apiDefinition), [node, apiDefinition]); useWriteApiDefinitionAtom(apiDefinition); diff --git a/packages/ui/app/src/playground/hooks/usePreloadApiLeaf.ts b/packages/ui/app/src/playground/hooks/usePreloadApiLeaf.ts index 4c21cbfc48..2cd5fdf615 100644 --- a/packages/ui/app/src/playground/hooks/usePreloadApiLeaf.ts +++ b/packages/ui/app/src/playground/hooks/usePreloadApiLeaf.ts @@ -14,12 +14,12 @@ export function usePreloadApiLeaf(): (node: NavigationNodeApiLeaf) => Promise { const route = selectApiRoute( get, - `/api/fern-docs/api-definition/${encodeURIComponent(node.apiDefinitionId)}/${visitDiscriminatedUnion( + `/api/fern-docs/api-definition?api=${encodeURIComponent(node.apiDefinitionId)}&${visitDiscriminatedUnion( node, )._visit({ - endpoint: (node) => `endpoint/${encodeURIComponent(node.endpointId)}`, - webSocket: (node) => `websocket/${encodeURIComponent(node.webSocketId)}`, - webhook: (node) => `webhook/${encodeURIComponent(node.webhookId)}`, + endpoint: (node) => `endpoint=${encodeURIComponent(node.endpointId)}`, + webSocket: (node) => `websocket=${encodeURIComponent(node.webSocketId)}`, + webhook: (node) => `webhook=${encodeURIComponent(node.webhookId)}`, })}`, ); const apiDefinition = await preload(route, fetcher); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts index f7e2515a90..87233d3507 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition.ts @@ -18,21 +18,12 @@ const resolveApiHandler: NextApiHandler = async ( res: NextApiResponse, ) => { const xFernHost = getDocsDomainNode(req); - if (req.method !== "POST") { + if (req.method !== "GET") { res.status(400).end(); return; } - const body = schema.safeParse(req.body); - - if (!body.success) { - // eslint-disable-next-line no-console - console.error(body.error); - res.status(400).end(); - return; - } - - const { api, endpoint, websocket, webhook } = body.data; + const { api, endpoint, websocket, webhook } = schema.parse(req.query); const flags = await getFeatureFlags(xFernHost);