From 43f193fd92199a56d13e60d3ca7b5c6687fa7d5c Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Mon, 18 Nov 2024 11:00:11 -0500 Subject: [PATCH 1/2] feat: api playground state injection --- packages/ui/fern-docs-auth/src/types.ts | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/ui/fern-docs-auth/src/types.ts b/packages/ui/fern-docs-auth/src/types.ts index c2de8c6534..b6d66d0fa9 100644 --- a/packages/ui/fern-docs-auth/src/types.ts +++ b/packages/ui/fern-docs-auth/src/types.ts @@ -9,7 +9,56 @@ export const FernUserSchema = z.object({ "The roles of the token (can be a string or an array of strings) which limits what content users can access", ) .optional(), + + // TODO: deprecate this api_key: z.string().optional().describe("For API Playground key injection"), + + /** + * when the user logs in, there may be some initial state in the API playground that we can replace + * with the user's information (i.e. api key, organization, project id, etc.) + * the initial state will be merged into each request if it's compatible with the api endpoint's spec. + * + * Example claim: + * ``` + * { + * "playground": { + * "initial_state": { + * "auth": { + * "bearer_token": "abc123" + * } + * } + * } + * } + */ + playground: z + .object({ + initial_state: z + .object({ + auth: z + .object({ + bearer_token: z.string().optional().describe("Bearer token to set in the request"), + basic: z + .object({ + username: z.string(), + password: z.string(), + }) + .optional(), + }) + .optional(), + headers: z.record(z.string(), z.string()).optional().describe("Headers to set in the request"), + path_parameters: z + .record(z.string(), z.any()) + .optional() + .describe("Path parameters to set in the request"), + query_parameters: z + .record(z.string(), z.any()) + .optional() + .describe("Query parameters to set in the request"), + // TODO: support body injection (potentially leveraging jsonpath?) — need a way to support different content types, and different spec types + }) + .optional(), + }) + .optional(), }); export const PathnameViewerRulesSchema = z.object({ From 5476160b0e6fb09a2cc0948b5b9e756423861485 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Mon, 18 Nov 2024 17:49:20 -0500 Subject: [PATCH 2/2] feat: api key injection into api playground from jwt --- packages/ui/app/src/atoms/playground.ts | 64 +++++++++-- packages/ui/app/src/playground/WithLabel.tsx | 2 +- .../auth/PlaygroundCardTriggerManual.tsx | 3 +- .../auth/PlaygroundHeaderAuthForm.tsx | 15 ++- .../src/playground/code-snippets/resolver.ts | 3 +- .../endpoint/PlaygroundEndpoint.tsx | 23 ++-- .../endpoint/PlaygroundEndpointForm.tsx | 8 +- .../form/PlaygroundObjectPropertyForm.tsx | 7 +- packages/ui/app/src/playground/types/auth.ts | 2 +- .../app/src/playground/utils/auth-headers.ts | 19 ++-- .../ui/app/src/playground/utils/endpoints.ts | 106 +++++++++++------- .../src/playground/utils/header-key-case.ts | 10 ++ .../ui/app/src/playground/utils/websocket.ts | 50 +++++++-- packages/ui/app/src/type-shorthand/index.tsx | 11 +- 14 files changed, 227 insertions(+), 96 deletions(-) create mode 100644 packages/ui/app/src/playground/utils/header-key-case.ts diff --git a/packages/ui/app/src/atoms/playground.ts b/packages/ui/app/src/atoms/playground.ts index d45842bd32..db376db27b 100644 --- a/packages/ui/app/src/atoms/playground.ts +++ b/packages/ui/app/src/atoms/playground.ts @@ -28,10 +28,11 @@ import { type PlaygroundWebSocketRequestFormState, } from "../playground/types"; import { - getInitialEndpointRequestFormState, getInitialEndpointRequestFormStateWithExample, getInitialWebSocketRequestFormState, } from "../playground/utils"; +import { pascalCaseHeaderKeys } from "../playground/utils/header-key-case"; +import { FERN_USER_ATOM } from "./auth"; import { FEATURE_FLAGS_ATOM } from "./flags"; import { useAtomEffect } from "./hooks"; import { HEADER_HEIGHT_ATOM } from "./layout"; @@ -177,7 +178,12 @@ export const PLAYGROUND_AUTH_STATE_ATOM = atomWithStorageValidation get(PLAYGROUND_AUTH_STATE_ATOM).bearerAuth ?? PLAYGROUND_AUTH_STATE_BEARER_TOKEN_INITIAL, + (get) => ({ + token: + get(PLAYGROUND_AUTH_STATE_ATOM).bearerAuth?.token ?? + get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.bearer_token ?? + "", + }), (_get, set, update: SetStateAction) => { set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({ ...prev, @@ -190,7 +196,12 @@ export const PLAYGROUND_AUTH_STATE_BEARER_TOKEN_ATOM = atom( ); export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom( - (get) => get(PLAYGROUND_AUTH_STATE_ATOM).header ?? PLAYGROUND_AUTH_STATE_HEADER_INITIAL, + (get) => ({ + headers: pascalCaseHeaderKeys({ + ...(get(PLAYGROUND_AUTH_STATE_ATOM).header?.headers ?? + get(FERN_USER_ATOM)?.playground?.initial_state?.headers), + }), + }), (_get, set, update: SetStateAction) => { set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({ ...prev, @@ -200,7 +211,16 @@ export const PLAYGROUND_AUTH_STATE_HEADER_ATOM = atom( ); export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_ATOM = atom( - (get) => get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth ?? PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL, + (get) => ({ + username: + get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.username ?? + get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.username ?? + "", + password: + get(PLAYGROUND_AUTH_STATE_ATOM).basicAuth?.password ?? + get(FERN_USER_ATOM)?.playground?.initial_state?.auth?.basic?.password ?? + "", + }), (_get, set, update: SetStateAction) => { set(PLAYGROUND_AUTH_STATE_ATOM, (prev) => ({ ...prev, @@ -265,6 +285,8 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA return; } + const playgroundInitialState = get(FERN_USER_ATOM)?.playground?.initial_state; + if (node.type === "endpoint") { const context = createEndpointContext(node, definition); @@ -277,7 +299,11 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA set( formStateAtom, - getInitialEndpointRequestFormStateWithExample(context, context.endpoint.examples?.[0]), + getInitialEndpointRequestFormStateWithExample( + context, + context.endpoint.examples?.[0], + playgroundInitialState, + ), ); } else if (node.type === "webSocket") { const context = createWebSocketContext(node, definition); @@ -290,7 +316,7 @@ export function useSetAndOpenPlayground(): (node: FernNavigation.NavigationNodeA return; } - set(formStateAtom, getInitialWebSocketRequestFormState(context)); + set(formStateAtom, getInitialWebSocketRequestFormState(context, playgroundInitialState)); } playgroundFormStateFamily.remove(node.id); }, @@ -304,9 +330,16 @@ export function usePlaygroundEndpointFormState( ): [PlaygroundEndpointRequestFormState, Dispatch>] { const formStateAtom = playgroundFormStateFamily(ctx.node.id); const formState = useAtomValue(formStateAtom); + const user = useAtomValue(FERN_USER_ATOM); return [ - formState?.type === "endpoint" ? formState : getInitialEndpointRequestFormState(ctx), + formState?.type === "endpoint" + ? formState + : getInitialEndpointRequestFormStateWithExample( + ctx, + ctx.endpoint.examples?.[0], + user?.playground?.initial_state, + ), useAtomCallback( useCallbackOne( (get, set, update: SetStateAction) => { @@ -316,12 +349,16 @@ export function usePlaygroundEndpointFormState( ? update( currentFormState?.type === "endpoint" ? currentFormState - : getInitialEndpointRequestFormState(ctx), + : getInitialEndpointRequestFormStateWithExample( + ctx, + ctx.endpoint.examples?.[0], + user?.playground?.initial_state, + ), ) : update; set(formStateAtom, newFormState); }, - [formStateAtom, ctx], + [formStateAtom, ctx, user?.playground?.initial_state], ), ), ]; @@ -332,9 +369,12 @@ export function usePlaygroundWebsocketFormState( ): [PlaygroundWebSocketRequestFormState, Dispatch>] { const formStateAtom = playgroundFormStateFamily(context.node.id); const formState = useAtomValue(playgroundFormStateFamily(context.node.id)); + const user = useAtomValue(FERN_USER_ATOM); return [ - formState?.type === "websocket" ? formState : getInitialWebSocketRequestFormState(context), + formState?.type === "websocket" + ? formState + : getInitialWebSocketRequestFormState(context, user?.playground?.initial_state), useAtomCallback( useCallbackOne( (get, set, update: SetStateAction) => { @@ -344,12 +384,12 @@ export function usePlaygroundWebsocketFormState( ? update( currentFormState?.type === "websocket" ? currentFormState - : getInitialWebSocketRequestFormState(context), + : getInitialWebSocketRequestFormState(context, user?.playground?.initial_state), ) : update; set(formStateAtom, newFormState); }, - [formStateAtom, context], + [formStateAtom, context, user?.playground?.initial_state], ), ), ]; diff --git a/packages/ui/app/src/playground/WithLabel.tsx b/packages/ui/app/src/playground/WithLabel.tsx index 23dc8224b7..cf6c476c05 100644 --- a/packages/ui/app/src/playground/WithLabel.tsx +++ b/packages/ui/app/src/playground/WithLabel.tsx @@ -48,7 +48,7 @@ export const WithLabel: FC> = ({ isRequired={!unwrapped.isOptional} isList={unwrapped.shape.type === "list"} isBoolean={unwrapped.shape.type === "primitive" && unwrapped.shape.value.type === "boolean"} - typeShorthand={renderTypeShorthandRoot(unwrapped.shape, types)} + typeShorthand={renderTypeShorthandRoot(property.valueShape, types, false, true)} > {children} diff --git a/packages/ui/app/src/playground/auth/PlaygroundCardTriggerManual.tsx b/packages/ui/app/src/playground/auth/PlaygroundCardTriggerManual.tsx index e2ad97d6c4..82506eb299 100644 --- a/packages/ui/app/src/playground/auth/PlaygroundCardTriggerManual.tsx +++ b/packages/ui/app/src/playground/auth/PlaygroundCardTriggerManual.tsx @@ -6,6 +6,7 @@ import { useAtomValue } from "jotai"; import { ReactElement } from "react"; import { PLAYGROUND_AUTH_STATE_ATOM } from "../../atoms"; import { PlaygroundAuthState } from "../types"; +import { pascalCaseHeaderKey } from "../utils/header-key-case"; interface PlaygroundCardTriggerManualProps { auth: APIV1Read.ApiAuth; @@ -74,7 +75,7 @@ function isAuthed(auth: APIV1Read.ApiAuth, authState: PlaygroundAuthState): bool bearerAuth: () => !isEmpty(authState.bearerAuth?.token.trim()), basicAuth: () => !isEmpty(authState.basicAuth?.username.trim()) && !isEmpty(authState.basicAuth?.password.trim()), - header: (header) => !isEmpty(authState.header?.headers[header.headerWireValue]?.trim()), + header: (header) => !isEmpty(authState.header?.headers[pascalCaseHeaderKey(header.headerWireValue)]?.trim()), oAuth: () => { const authToken = authState.oauth?.selectedInputMethod === "credentials" diff --git a/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx b/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx index bb5f447b3e..045d3fd930 100644 --- a/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx +++ b/packages/ui/app/src/playground/auth/PlaygroundHeaderAuthForm.tsx @@ -1,10 +1,12 @@ import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { unknownToString } from "@fern-api/ui-core-utils"; import { atom } from "jotai"; import { useAtom } from "jotai/react"; import type { ReactElement, SetStateAction } from "react"; import { useMemoOne } from "use-memo-one"; import { PLAYGROUND_AUTH_STATE_HEADER_ATOM } from "../../atoms"; import { PasswordInputGroup } from "../PasswordInputGroup"; +import { pascalCaseHeaderKey } from "../utils/header-key-case"; export function PlaygroundHeaderAuthForm({ header, @@ -17,14 +19,15 @@ export function PlaygroundHeaderAuthForm({ useMemoOne( () => atom( - (get) => get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[header.headerWireValue], + (get) => + get(PLAYGROUND_AUTH_STATE_HEADER_ATOM).headers[pascalCaseHeaderKey(header.headerWireValue)], (_get, set, change: SetStateAction) => { set(PLAYGROUND_AUTH_STATE_HEADER_ATOM, ({ headers }) => ({ headers: { ...headers, - [header.headerWireValue]: + [pascalCaseHeaderKey(header.headerWireValue)]: typeof change === "function" - ? change(headers[header.headerWireValue] ?? "") + ? change(headers[pascalCaseHeaderKey(header.headerWireValue)] ?? "") : change, }, })); @@ -37,12 +40,14 @@ export function PlaygroundHeaderAuthForm({ return (
  • { + const user = useAtomValue(FERN_USER_ATOM); const { node, endpoint, auth } = context; const [formState, setFormState] = usePlaygroundEndpointFormState(context); const resetWithExample = useEventCallback(() => { - setFormState(getInitialEndpointRequestFormStateWithExample(context, context.endpoint.examples?.[0])); + setFormState( + getInitialEndpointRequestFormStateWithExample( + context, + context.endpoint.examples?.[0], + user?.playground?.initial_state, + ), + ); }); const resetWithoutExample = useEventCallback(() => { - setFormState(getInitialEndpointRequestFormState(context)); + setFormState( + getInitialEndpointRequestFormStateWithExample(context, undefined, user?.playground?.initial_state), + ); }); const basePath = useBasePath(); diff --git a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx index 257a34e615..cf90c089e6 100644 --- a/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx +++ b/packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx @@ -1,10 +1,11 @@ -import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition"; +import { PropertyKey, type EndpointContext } from "@fern-api/fdr-sdk/api-definition"; import { EMPTY_ARRAY, visitDiscriminatedUnion } from "@fern-api/ui-core-utils"; import { Dispatch, FC, SetStateAction, useCallback, useMemo } from "react"; import { PlaygroundFileUploadForm } from "../form/PlaygroundFileUploadForm"; import { PlaygroundObjectForm } from "../form/PlaygroundObjectForm"; import { PlaygroundObjectPropertiesForm } from "../form/PlaygroundObjectPropertyForm"; import { PlaygroundEndpointRequestFormState, PlaygroundFormStateBody } from "../types"; +import { pascalCaseHeaderKey } from "../utils/header-key-case"; import { PlaygroundEndpointAliasForm } from "./PlaygroundEndpointAliasForm"; import { PlaygroundEndpointFormSection } from "./PlaygroundEndpointFormSection"; import { PlaygroundEndpointMultipartForm } from "./PlaygroundEndpointMultipartForm"; @@ -104,7 +105,10 @@ export const PlaygroundEndpointForm: FC = ({ ({ + ...header, + key: PropertyKey(pascalCaseHeaderKey(header.key)), + }))} extraProperties={undefined} onChange={setHeaders} value={formState?.headers} diff --git a/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx b/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx index 5e5438213e..1e162572f2 100644 --- a/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx +++ b/packages/ui/app/src/playground/form/PlaygroundObjectPropertyForm.tsx @@ -129,7 +129,12 @@ export const PlaygroundObjectPropertiesForm = memo : undefined, }), diff --git a/packages/ui/app/src/playground/types/auth.ts b/packages/ui/app/src/playground/types/auth.ts index 1dd9257cac..76bfcecfed 100644 --- a/packages/ui/app/src/playground/types/auth.ts +++ b/packages/ui/app/src/playground/types/auth.ts @@ -19,7 +19,7 @@ export const PlaygroundAuthStateBasicAuthSchema = z.strictObject({ export type PlaygroundAuthStateBasicAuth = z.infer; export const PLAYGROUND_AUTH_STATE_BASIC_AUTH_INITIAL: PlaygroundAuthStateBasicAuth = { username: "", password: "" }; -export const PlaygroundAuthStateOAuthSchema = z.strictObject({ +export const PlaygroundAuthStateOAuthSchema = z.object({ clientId: z.string(), clientSecret: z.string(), accessToken: z.string(), diff --git a/packages/ui/app/src/playground/utils/auth-headers.ts b/packages/ui/app/src/playground/utils/auth-headers.ts index 55d921eda8..077fb88624 100644 --- a/packages/ui/app/src/playground/utils/auth-headers.ts +++ b/packages/ui/app/src/playground/utils/auth-headers.ts @@ -3,6 +3,7 @@ import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedU import { decodeJwt } from "jose"; import { noop } from "ts-essentials"; import { PlaygroundAuthState } from "../types"; +import { pascalCaseHeaderKey } from "./header-key-case"; import { OAuthClientCredentialReferencedEndpointLoginFlowProps, oAuthClientCredentialReferencedEndpointLoginFlow, @@ -30,11 +31,13 @@ export function buildAuthHeaders( headers["Authorization"] = `Bearer ${token}`; }, header: (header) => { - let value = authState.header?.headers[header.headerWireValue] ?? ""; + // pluck the value from the headers object (avoid inheriting all the other headers) + let value = authState.header?.headers[pascalCaseHeaderKey(header.headerWireValue)] ?? ""; if (redacted) { value = obfuscateSecret(value); } - headers[header.headerWireValue] = header.prefix != null ? `${header.prefix} ${value}` : value; + headers[pascalCaseHeaderKey(header.headerWireValue)] = + header.prefix != null ? `${header.prefix} ${value}` : value; }, basicAuth: () => { const username = authState.basicAuth?.username ?? ""; @@ -77,14 +80,10 @@ export function buildAuthHeaders( } catch {} } - const tokenPrefix = authState.oauth?.tokenPrefix?.length - ? authState.oauth.tokenPrefix - : "Bearer"; - if (redacted) { - headers["Authorization"] = `${tokenPrefix} ${obfuscateSecret(token)}`; - } else { - headers["Authorization"] = `${tokenPrefix} ${token}`; - } + const tokenPrefix = authState.oauth?.tokenPrefix || "Bearer"; + + headers["Authorization"] = + `${tokenPrefix.length ? `${tokenPrefix} ` : ""}${redacted ? obfuscateSecret(token) : token}`; }, }); }, diff --git a/packages/ui/app/src/playground/utils/endpoints.ts b/packages/ui/app/src/playground/utils/endpoints.ts index 029146e9c4..e30e678606 100644 --- a/packages/ui/app/src/playground/utils/endpoints.ts +++ b/packages/ui/app/src/playground/utils/endpoints.ts @@ -1,56 +1,76 @@ -import type { EndpointContext } from "@fern-api/fdr-sdk/api-definition"; +import type { EndpointContext, ObjectProperty } from "@fern-api/fdr-sdk/api-definition"; import { ExampleEndpointCall } from "@fern-api/fdr-sdk/api-definition"; import { EMPTY_OBJECT } from "@fern-api/ui-core-utils"; -import { mapValues } from "es-toolkit/object"; -import { PlaygroundEndpointRequestFormState, PlaygroundFormDataEntryValue } from "../types"; +import { FernUser } from "@fern-ui/fern-docs-auth"; +import { compact } from "es-toolkit/array"; +import { mapValues, pick } from "es-toolkit/object"; +import type { PlaygroundEndpointRequestFormState, PlaygroundFormDataEntryValue } from "../types"; import { getEmptyValueForHttpRequestBody, getEmptyValueForObjectProperties } from "./default-values"; - -export function getInitialEndpointRequestFormState( - ctx: EndpointContext | undefined, -): PlaygroundEndpointRequestFormState { - return { - type: "endpoint", - headers: getEmptyValueForObjectProperties( - [...(ctx?.globalHeaders ?? []), ...(ctx?.endpoint?.requestHeaders ?? [])], - ctx?.types ?? EMPTY_OBJECT, - ), - pathParameters: getEmptyValueForObjectProperties(ctx?.endpoint?.pathParameters, ctx?.types ?? EMPTY_OBJECT), - queryParameters: getEmptyValueForObjectProperties(ctx?.endpoint?.queryParameters, ctx?.types ?? EMPTY_OBJECT), - body: getEmptyValueForHttpRequestBody(ctx?.endpoint?.request?.body, ctx?.types ?? EMPTY_OBJECT), - }; -} +import { pascalCaseHeaderKeys } from "./header-key-case"; export function getInitialEndpointRequestFormStateWithExample( context: EndpointContext | undefined, exampleCall: ExampleEndpointCall | undefined, + playgroundInitialState: NonNullable["initial_state"] | undefined, ): PlaygroundEndpointRequestFormState { - if (exampleCall == null) { - return getInitialEndpointRequestFormState(context); - } return { type: "endpoint", - headers: exampleCall.headers ?? {}, - pathParameters: exampleCall.pathParameters ?? {}, - queryParameters: exampleCall.queryParameters ?? {}, + headers: { + ...pascalCaseHeaderKeys( + getEmptyValueForObjectProperties( + compact([context?.globalHeaders, context?.endpoint.requestHeaders]).flat(), + context?.types ?? EMPTY_OBJECT, + ), + ), + ...pascalCaseHeaderKeys(exampleCall?.headers ?? {}), + ...pascalCaseHeaderKeys( + filterParams( + playgroundInitialState?.headers ?? {}, + compact([context?.globalHeaders, context?.endpoint.requestHeaders]).flat(), + ), + ), + }, + pathParameters: { + ...getEmptyValueForObjectProperties(context?.endpoint.pathParameters ?? [], context?.types ?? EMPTY_OBJECT), + ...exampleCall?.pathParameters, + ...filterParams(playgroundInitialState?.path_parameters ?? {}, context?.endpoint.pathParameters ?? []), + }, + queryParameters: { + ...getEmptyValueForObjectProperties( + context?.endpoint.queryParameters ?? [], + context?.types ?? EMPTY_OBJECT, + ), + ...exampleCall?.queryParameters, + ...filterParams(playgroundInitialState?.query_parameters ?? {}, context?.endpoint.queryParameters ?? []), + }, body: - exampleCall.requestBody?.type === "form" - ? { - type: "form-data", - value: mapValues( - exampleCall.requestBody.value, - (exampleValue): PlaygroundFormDataEntryValue => - exampleValue.type === "filename" || exampleValue.type === "filenameWithData" - ? { type: "file", value: undefined } - : exampleValue.type === "filenames" || exampleValue.type === "filenamesWithData" - ? { type: "fileArray", value: [] } - : { type: "json", value: exampleValue.value }, - ), - } - : exampleCall.requestBody?.type === "bytes" - ? { - type: "octet-stream", - value: undefined, - } - : { type: "json", value: exampleCall.requestBody?.value }, + exampleCall != null + ? exampleCall?.requestBody?.type === "form" + ? { + type: "form-data", + value: mapValues( + exampleCall.requestBody.value, + (exampleValue): PlaygroundFormDataEntryValue => + exampleValue.type === "filename" || exampleValue.type === "filenameWithData" + ? { type: "file", value: undefined } + : exampleValue.type === "filenames" || exampleValue.type === "filenamesWithData" + ? { type: "fileArray", value: [] } + : { type: "json", value: exampleValue.value }, + ), + } + : exampleCall?.requestBody?.type === "bytes" + ? { type: "octet-stream", value: undefined } + : { type: "json", value: exampleCall?.requestBody?.value } + : getEmptyValueForHttpRequestBody(context?.endpoint.request?.body, context?.types ?? EMPTY_OBJECT), }; } + +function filterParams( + initialStateParams: Record, + requestParams: ObjectProperty[], +): Record { + return pick( + initialStateParams, + requestParams.map((param) => param.key), + ); +} diff --git a/packages/ui/app/src/playground/utils/header-key-case.ts b/packages/ui/app/src/playground/utils/header-key-case.ts new file mode 100644 index 0000000000..544f86eaee --- /dev/null +++ b/packages/ui/app/src/playground/utils/header-key-case.ts @@ -0,0 +1,10 @@ +import { mapKeys } from "es-toolkit/object"; +import { pascalCase } from "es-toolkit/string"; + +export function pascalCaseHeaderKey(key: string): string { + return key.split("-").map(pascalCase).join("-"); +} + +export function pascalCaseHeaderKeys(headers: Record = {}): Record { + return mapKeys(headers, (_, key) => pascalCaseHeaderKey(key)); +} diff --git a/packages/ui/app/src/playground/utils/websocket.ts b/packages/ui/app/src/playground/utils/websocket.ts index 4c3a9065b8..c0647cb148 100644 --- a/packages/ui/app/src/playground/utils/websocket.ts +++ b/packages/ui/app/src/playground/utils/websocket.ts @@ -1,16 +1,40 @@ -import type { WebSocketContext } from "@fern-api/fdr-sdk/api-definition"; +import type { ObjectProperty, WebSocketContext } from "@fern-api/fdr-sdk/api-definition"; +import { EMPTY_OBJECT } from "@fern-api/ui-core-utils"; +import { FernUser } from "@fern-ui/fern-docs-auth"; +import { compact } from "es-toolkit/array"; +import { pick } from "es-toolkit/object"; import { PlaygroundWebSocketRequestFormState } from "../types"; import { getEmptyValueForObjectProperties, getEmptyValueForType } from "./default-values"; +import { pascalCaseHeaderKeys } from "./header-key-case"; -export function getInitialWebSocketRequestFormState(context: WebSocketContext): PlaygroundWebSocketRequestFormState { +export function getInitialWebSocketRequestFormState( + context: WebSocketContext, + playgroundInitialState: NonNullable["initial_state"] | undefined, +): PlaygroundWebSocketRequestFormState { return { type: "websocket", - headers: getEmptyValueForObjectProperties( - [...context.globalHeaders, ...(context.channel.requestHeaders ?? [])], - context.types, - ), - pathParameters: getEmptyValueForObjectProperties(context.channel.pathParameters, context.types), - queryParameters: getEmptyValueForObjectProperties(context.channel.queryParameters, context.types), + headers: { + ...pascalCaseHeaderKeys( + getEmptyValueForObjectProperties( + compact([context.globalHeaders, context.channel.requestHeaders]).flat(), + context.types ?? EMPTY_OBJECT, + ), + ), + ...pascalCaseHeaderKeys( + filterParams( + playgroundInitialState?.headers ?? {}, + compact([context.globalHeaders, context.channel.requestHeaders]).flat(), + ), + ), + }, + pathParameters: { + ...getEmptyValueForObjectProperties(context.channel.pathParameters, context.types ?? EMPTY_OBJECT), + ...filterParams(playgroundInitialState?.path_parameters ?? {}, context.channel.pathParameters ?? []), + }, + queryParameters: { + ...getEmptyValueForObjectProperties(context.channel.queryParameters, context.types ?? EMPTY_OBJECT), + ...filterParams(playgroundInitialState?.query_parameters ?? {}, context.channel.queryParameters ?? []), + }, messages: Object.fromEntries( context.channel.messages .filter((message) => message.origin === "client") @@ -18,3 +42,13 @@ export function getInitialWebSocketRequestFormState(context: WebSocketContext): ), }; } + +function filterParams( + initialStateParams: Record, + requestParams: ObjectProperty[], +): Record { + return pick( + initialStateParams, + requestParams.map((param) => param.key), + ); +} diff --git a/packages/ui/app/src/type-shorthand/index.tsx b/packages/ui/app/src/type-shorthand/index.tsx index 8632d1862b..75fd25b546 100644 --- a/packages/ui/app/src/type-shorthand/index.tsx +++ b/packages/ui/app/src/type-shorthand/index.tsx @@ -13,6 +13,7 @@ export function renderTypeShorthandRoot( shape: TypeShapeOrReference, types: Record, isResponse: boolean = false, + hideOptional: boolean = false, ): ReactNode { const unwrapped = unwrapReference(shape, types); const typeShorthand = renderTypeShorthand(unwrapped.shape, { nullable: isResponse }, types); @@ -20,10 +21,16 @@ export function renderTypeShorthandRoot( {typeShorthand} {unwrapped.isOptional ? ( - Optional + !hideOptional ? ( + Optional + ) : ( + false + ) ) : !isResponse ? ( Required - ) : null} + ) : ( + false + )} {unwrapped.shape.type === "primitive" && toPrimitiveTypeLabels({ primitive: unwrapped.shape.value }).map((label, index) => ( {label}