From cb800d36a31d8341546b9138e5ec799432677455 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 16 Jan 2025 14:32:07 -0500 Subject: [PATCH 01/45] feat(laboratory/preflight-sandbox): allow appending headers --- packages/web/app/src/lib/kit/headers.ts | 14 +++ packages/web/app/src/lib/kit/index.ts | 5 + packages/web/app/src/lib/kit/never.ts | 16 ++++ packages/web/app/src/lib/kit/types/json.ts | 19 ++++ .../lib/preflight-sandbox/graphiql-plugin.tsx | 91 ++++++++++++++----- .../preflight-script-worker.ts | 55 +++++++++-- .../preflight-worker-embed.ts | 5 +- .../src/lib/preflight-sandbox/shared-types.ts | 58 ++++++++---- .../web/app/src/pages/target-laboratory.tsx | 26 +++++- 9 files changed, 232 insertions(+), 57 deletions(-) create mode 100644 packages/web/app/src/lib/kit/headers.ts create mode 100644 packages/web/app/src/lib/kit/index.ts create mode 100644 packages/web/app/src/lib/kit/never.ts create mode 100644 packages/web/app/src/lib/kit/types/json.ts diff --git a/packages/web/app/src/lib/kit/headers.ts b/packages/web/app/src/lib/kit/headers.ts new file mode 100644 index 0000000000..00d8d56c7b --- /dev/null +++ b/packages/web/app/src/lib/kit/headers.ts @@ -0,0 +1,14 @@ +export namespace Headers { + /** + * Take given HeadersInit and append it (mutating) into given Headers. + * + * @param headers - The Headers object to append to. + * @param headersInit - The HeadersInit object to append from. + */ + export const appendInit = (headers: Headers, headersInit: HeadersInit): void => { + const newHeaders = new globalThis.Headers(headersInit); + newHeaders.forEach((value, key) => { + headers.append(key, value); + }); + }; +} diff --git a/packages/web/app/src/lib/kit/index.ts b/packages/web/app/src/lib/kit/index.ts new file mode 100644 index 0000000000..a649ddba3e --- /dev/null +++ b/packages/web/app/src/lib/kit/index.ts @@ -0,0 +1,5 @@ +export * as Kit from './index'; + +export * from './never'; +export * from './headers'; +export * from './types/json'; diff --git a/packages/web/app/src/lib/kit/never.ts b/packages/web/app/src/lib/kit/never.ts new file mode 100644 index 0000000000..8d03db4669 --- /dev/null +++ b/packages/web/app/src/lib/kit/never.ts @@ -0,0 +1,16 @@ +/** + * This case is impossible. + * If it is, then there is a bug in our code. + */ +export const neverCase = (value: never): never => { + never(`Unhandled case: ${String(value)}`); +}; + +/** + * This code cannot be reached. + * If it can be, then there is a bug in our code. + */ +export const never: (contextMessage?: string) => never = contextMessage => { + contextMessage = contextMessage ?? '(no additional context provided)'; + throw new Error(`Something that should be impossible happened: ${contextMessage}`); +}; diff --git a/packages/web/app/src/lib/kit/types/json.ts b/packages/web/app/src/lib/kit/types/json.ts new file mode 100644 index 0000000000..d8962d0dd2 --- /dev/null +++ b/packages/web/app/src/lib/kit/types/json.ts @@ -0,0 +1,19 @@ +export namespace JSON { + export const encode = (value: value): string => { + return globalThis.JSON.stringify(value); + }; + + export const encodePretty = (value: value): string => { + return globalThis.JSON.stringify(value, null, 2); + }; + + export const decode = (value: string): Value => { + return globalThis.JSON.parse(value); + }; + + export type Value = PrimitiveValue | NonPrimitiveValue; + + export type NonPrimitiveValue = { [key: string]: Value } | Array; + + export type PrimitiveValue = string | number | boolean | null; +} diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index c84f12ca6f..f46f0d1223 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -38,10 +38,13 @@ import { TriangleRightIcon, } from '@radix-ui/react-icons'; import { useParams } from '@tanstack/react-router'; +import { Kit } from '../kit'; import { cn } from '../utils'; import type { LogMessage } from './preflight-script-worker'; import { IFrameEvents } from './shared-types'; +type Result = Omit; + export const preflightScriptPlugin: GraphiQLPlugin = { icon: () => ( | null { +function safeParseJSON<$UnsafeCast extends Kit.JSON.Value = Kit.JSON.Value>( + str: string, +): $UnsafeCast | null { try { - return JSON.parse(str); + return Kit.JSON.decode(str) as $UnsafeCast; } catch { return null; } @@ -165,23 +170,47 @@ export function usePreflightScript(args: { 'hive:laboratory:isPreflightScriptEnabled', false, ); - const [environmentVariables, setEnvironmentVariables] = useLocalStorage( - 'hive:laboratory:environment', - '', - ); + + // ------------ + // Result State + // ------------ + // + // todo: Probably better to store the result as a single JSON object value. + // Use a proper versioned schema with codecs for coercing, defaults, validation, etc. + // + // todo: Improve `useLocalStorage` by allowing passing a codec? Then we can co-locate + // the codec with the data and have it applied transparently, use a decoded value + // for the default, etc. ? + + const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', '{}'); // prettier-ignore const latestEnvironmentVariablesRef = useRef(environmentVariables); - useEffect(() => { - latestEnvironmentVariablesRef.current = environmentVariables; - }); + useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore + const decodeEnvironmentVariables = (encoded: string) => safeParseJSON(encoded) ?? {}; // prettier-ignore + + const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', '[]'); + const latestHeadersRef = useRef(headers); + useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore + const decodeHeaders = (encoded: string) => safeParseJSON(encoded) ?? []; + + const decodeResult = (): Result => { + return { + environmentVariables: decodeEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore + headers: decodeHeaders(latestHeadersRef.current), + }; + }; + // ----------- const [state, setState] = useState(PreflightWorkerState.ready); const [logs, setLogs] = useState([]); const currentRun = useRef(null); - async function execute(script = target?.preflightScript?.sourceCode ?? '', isPreview = false) { + async function execute( + script = target?.preflightScript?.sourceCode ?? '', + isPreview = false, + ): Promise { if (isPreview === false && !isPreflightScriptEnabled) { - return safeParseJSON(latestEnvironmentVariablesRef.current); + return decodeResult(); } const id = crypto.randomUUID(); @@ -201,7 +230,7 @@ export function usePreflightScript(args: { type: IFrameEvents.Incoming.Event.run, id, script, - environmentVariables: (environmentVariables && safeParseJSON(environmentVariables)) || {}, + environmentVariables: decodeEnvironmentVariables(environmentVariables), } satisfies IFrameEvents.Incoming.EventData, '*', ); @@ -257,16 +286,20 @@ export function usePreflightScript(args: { } if (ev.data.type === IFrameEvents.Outgoing.Event.result) { - const mergedEnvironmentVariables = JSON.stringify( - { - ...safeParseJSON(latestEnvironmentVariablesRef.current), - ...ev.data.environmentVariables, - }, - null, - 2, - ); - setEnvironmentVariables(mergedEnvironmentVariables); - latestEnvironmentVariablesRef.current = mergedEnvironmentVariables; + const mergedEnvironmentVariablesEncoded = Kit.JSON.encodePretty({ + ...decodeEnvironmentVariables(latestEnvironmentVariablesRef.current), + ...ev.data.environmentVariables, + }); + setEnvironmentVariables(mergedEnvironmentVariablesEncoded); + latestEnvironmentVariablesRef.current = mergedEnvironmentVariablesEncoded; + + const mergedHeadersEncoded = Kit.JSON.encodePretty([ + ...decodeHeaders(latestHeadersRef.current), + ...ev.data.headers, + ]); + setHeaders(mergedHeadersEncoded); + latestHeadersRef.current = mergedHeadersEncoded; + setLogs(logs => [ ...logs, `> End running script. Done in ${(Date.now() - now) / 1000}s`, @@ -299,6 +332,16 @@ export function usePreflightScript(args: { setLogs(logs => [...logs, log]); return; } + + if (ev.data.type === IFrameEvents.Outgoing.Event.ready) { + return; + } + + if (ev.data.type === IFrameEvents.Outgoing.Event.start) { + return; + } + + Kit.neverCase(ev.data); } window.addEventListener('message', eventHandler); @@ -317,7 +360,7 @@ export function usePreflightScript(args: { window.removeEventListener('message', eventHandler); setState(PreflightWorkerState.ready); - return safeParseJSON(latestEnvironmentVariablesRef.current); + return decodeResult(); } catch (err) { if (err instanceof Error) { setLogs(prev => [ @@ -329,7 +372,7 @@ export function usePreflightScript(args: { }, ]); setState(PreflightWorkerState.ready); - return safeParseJSON(latestEnvironmentVariablesRef.current); + return decodeResult(); } throw err; } diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts index 95b358f332..754b3668e8 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -1,9 +1,15 @@ import CryptoJS from 'crypto-js'; import CryptoJSPackageJson from 'crypto-js/package.json'; +import { Kit } from '../kit'; import { ALLOWED_GLOBALS } from './allowed-globals'; import { isJSONPrimitive } from './json'; import { WorkerEvents } from './shared-types'; +interface WorkerData { + headers: Headers; + environmentVariables: Record; +} + export type LogMessage = string | Error; /** @@ -47,11 +53,14 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise { return; } - const { environmentVariables, script } = args; + const { script } = args; - // When running in worker `environmentVariables` will not be a reference to the main thread value - // but sometimes this will be tested outside the worker, so we don't want to mutate the input in that case - const workingEnvironmentVariables = { ...environmentVariables }; + const workerData: WorkerData = { + headers: new Headers(), + // When running in worker `environmentVariables` will not be a reference to the main thread value + // but sometimes this will be tested outside the worker, so we don't want to mutate the input in that case + environmentVariables: { ...args.environmentVariables }, + }; // generate list of all in scope variables, we do getOwnPropertyNames and `for in` because each contain slightly different sets of keys const allGlobalKeys = Object.getOwnPropertyNames(globalThis); @@ -116,17 +125,44 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise { }, environment: { get(key: string) { - return Object.freeze(workingEnvironmentVariables[key]); + return Object.freeze(workerData.environmentVariables[key]); }, set(key: string, value: unknown) { const validValue = getValidEnvVariable(value); if (validValue === undefined) { - delete workingEnvironmentVariables[key]; + delete workerData.environmentVariables[key]; } else { - workingEnvironmentVariables[key] = validValue; + workerData.environmentVariables[key] = validValue; } }, }, + /** + * Helpers for manipulating the request before it is sent. + */ + request: { + /** + * Helpers for manipulating the request headers. + */ + headers: { + /** + * Add one header to the request. + * + * @param header - The name of the header. + * @param value - The value of the header. + */ + add: (header: string, value: string) => { + workerData.headers.append(header, value); + }, + /** + * Add multiple headers to the request. + * + * @param headersInit - The {@link HeadersInit} to add. + */ + addMany: (headersInit: HeadersInit) => { + Kit.Headers.appendInit(workerData.headers, headersInit); + }, + }, + }, /** * Mimics the `prompt` function in the browser, by sending a message to the main thread * and waiting for a response. @@ -167,9 +203,12 @@ ${script}})()`; sendMessage({ type: WorkerEvents.Outgoing.Event.error, error: error as Error }); return; } + sendMessage({ type: WorkerEvents.Outgoing.Event.result, - environmentVariables: workingEnvironmentVariables, + // todo: We need to more precisely type environment value. Currently unknown. Why? + environmentVariables: workerData.environmentVariables as any, + headers: Array.from(workerData.headers.entries()), }); } diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts index b1aa78664e..51c29c3ead 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts @@ -1,3 +1,4 @@ +import { Kit } from '../kit'; import PreflightWorker from './preflight-script-worker?worker&inline'; import { IFrameEvents, WorkerEvents } from './shared-types'; @@ -103,9 +104,9 @@ function handleEvent(data: IFrameEvents.Incoming.EventData) { if (ev.data.type === WorkerEvents.Outgoing.Event.result) { postMessage({ + ...ev.data, type: IFrameEvents.Outgoing.Event.result, runId, - environmentVariables: ev.data.environmentVariables, }); terminate(); return; @@ -129,6 +130,8 @@ function handleEvent(data: IFrameEvents.Incoming.EventData) { terminate(); return; } + + Kit.neverCase(ev.data); }, ); diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index b96386e4b4..e3c123a3e5 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -28,12 +28,10 @@ export namespace IFrameEvents { log: string | Error; }; - type ResultEventData = { + export interface ResultEventData extends Omit { type: Event.result; runId: string; - environmentVariables: Record; - }; - + } type ErrorEventData = { type: Event.error; runId: string; @@ -100,23 +98,43 @@ export namespace WorkerEvents { prompt = 'prompt', } - type LogEventData = { type: Event.log; message: string }; - type ErrorEventData = { type: Event.error; error: Error }; - type PromptEventData = { - type: Event.prompt; - promptId: number; - message: string; - defaultValue: string; - }; - type ResultEventData = { type: Event.result; environmentVariables: Record }; - type ReadyEventData = { type: Event.ready }; + export namespace EventData { + export interface Log { + type: Event.log; + message: string; + } + + export interface Error { + type: Event.error; + error: globalThis.Error; + } + + export interface Prompt { + type: Event.prompt; + promptId: number; + message: string; + defaultValue: string; + } + + export interface Result { + type: Event.result; + environmentVariables: Record; + headers: [name: string, value: string][]; + } + + export interface Ready { + type: Event.ready; + } + } + + export type EventData = { + log: EventData.Log; + error: EventData.Error; + prompt: EventData.Prompt; + result: EventData.Result; + ready: EventData.Ready; + }[Event]; - export type EventData = - | ResultEventData - | LogEventData - | ErrorEventData - | ReadyEventData - | PromptEventData; export type MessageEvent = _MessageEvent; } diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 4bf168d8cd..84a146ba72 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -251,7 +251,7 @@ function Save(props: { ); } -function substituteVariablesInHeader( +function substituteVariablesInHeaders( headers: Record, environmentVariables: Record, ) { @@ -313,7 +313,7 @@ function LaboratoryPageContent(props: { const fetcher = useMemo(() => { return async (params, opts) => { - let headers = opts?.headers; + let headers = opts?.headers ?? {}; const url = (actualSelectedApiEndpoint === 'linkedApi' ? target?.graphqlEndpointUrl : undefined) ?? mockEndpoint; @@ -328,8 +328,26 @@ function LaboratoryPageContent(props: { }); try { const result = await preflightScript.execute(); - if (result && headers) { - headers = substituteVariablesInHeader(headers, result); + // todo: Why check `result` truthiness here if it can never by falsy? + if (result) { + // We merge the result headers into the fetcher headers before performing header variable substitution + // so that users can have a predictable experience working with headers regardless of the place those + // headers come from. + // + // todo: GraphiQLFetcher appears to only support record-shaped headers which seems wrong because it + // precludes complete usage of Headers data structure, namely where there are multiple values for one + // header. We could try to hack a solution here by doing merges of such cases but that seems + // likely to introduce more bugs given the different formats that different kinds of headers use to + // delineate multiple values. + // + // What should really happen is that GraphiQLFetcher accepts a HeadersInit type. + // + const newHeadersLossyFixMe = Object.fromEntries(result.headers); + headers = { + ...headers, + ...newHeadersLossyFixMe, + }; + headers = substituteVariablesInHeaders(headers, result.environmentVariables); } } catch (err: unknown) { if (err instanceof Error === false) { From fac4434e47f147516563aa2e8890ec6b3590fe8e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Fri, 17 Jan 2025 14:53:48 -0500 Subject: [PATCH 02/45] typo fix --- packages/web/app/src/lib/kit/never.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/app/src/lib/kit/never.ts b/packages/web/app/src/lib/kit/never.ts index 8d03db4669..b0bfb785cb 100644 --- a/packages/web/app/src/lib/kit/never.ts +++ b/packages/web/app/src/lib/kit/never.ts @@ -1,6 +1,6 @@ /** * This case is impossible. - * If it is, then there is a bug in our code. + * If it happens, then that means there is a bug in our code. */ export const neverCase = (value: never): never => { never(`Unhandled case: ${String(value)}`); @@ -8,7 +8,7 @@ export const neverCase = (value: never): never => { /** * This code cannot be reached. - * If it can be, then there is a bug in our code. + * If it is reached, then that means there is a bug in our code. */ export const never: (contextMessage?: string) => never = contextMessage => { contextMessage = contextMessage ?? '(no additional context provided)'; From 4084026a0014e45cddec57c29f763226ea7969a8 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:15:47 -0500 Subject: [PATCH 03/45] remove if --- .../web/app/src/pages/target-laboratory.tsx | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 84a146ba72..60bed5c8c0 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -328,27 +328,24 @@ function LaboratoryPageContent(props: { }); try { const result = await preflightScript.execute(); - // todo: Why check `result` truthiness here if it can never by falsy? - if (result) { - // We merge the result headers into the fetcher headers before performing header variable substitution - // so that users can have a predictable experience working with headers regardless of the place those - // headers come from. - // - // todo: GraphiQLFetcher appears to only support record-shaped headers which seems wrong because it - // precludes complete usage of Headers data structure, namely where there are multiple values for one - // header. We could try to hack a solution here by doing merges of such cases but that seems - // likely to introduce more bugs given the different formats that different kinds of headers use to - // delineate multiple values. - // - // What should really happen is that GraphiQLFetcher accepts a HeadersInit type. - // - const newHeadersLossyFixMe = Object.fromEntries(result.headers); - headers = { - ...headers, - ...newHeadersLossyFixMe, - }; - headers = substituteVariablesInHeaders(headers, result.environmentVariables); - } + // We merge the result headers into the fetcher headers before performing header variable substitution + // so that users can have a predictable experience working with headers regardless of the place those + // headers come from. + // + // todo: GraphiQLFetcher appears to only support record-shaped headers which seems wrong because it + // precludes complete usage of Headers data structure, namely where there are multiple values for one + // header. We could try to hack a solution here by doing merges of such cases but that seems + // likely to introduce more bugs given the different formats that different kinds of headers use to + // delineate multiple values. + // + // What should really happen is that GraphiQLFetcher accepts a HeadersInit type. + // + const newHeadersLossyFixMe = Object.fromEntries(result.headers); + headers = { + ...headers, + ...newHeadersLossyFixMe, + }; + headers = substituteVariablesInHeaders(headers, result.environmentVariables); } catch (err: unknown) { if (err instanceof Error === false) { throw err; From 0c6d4651eeae48d915104ab998d42b4778e645ad Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:29:01 -0500 Subject: [PATCH 04/45] expose headers --- .../preflight-script-worker.ts | 36 +++++++------------ .../src/lib/preflight-sandbox/shared-types.ts | 4 ++- .../web/app/src/pages/target-laboratory.tsx | 2 +- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts index 754b3668e8..47b36534db 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -6,7 +6,9 @@ import { isJSONPrimitive } from './json'; import { WorkerEvents } from './shared-types'; interface WorkerData { - headers: Headers; + request: { + headers: Headers; + }; environmentVariables: Record; } @@ -56,7 +58,9 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise { const { script } = args; const workerData: WorkerData = { - headers: new Headers(), + request: { + headers: new Headers(), + }, // When running in worker `environmentVariables` will not be a reference to the main thread value // but sometimes this will be tested outside the worker, so we don't want to mutate the input in that case environmentVariables: { ...args.environmentVariables }, @@ -137,31 +141,13 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise { }, }, /** - * Helpers for manipulating the request before it is sent. + * Contains aspects of the request that you can manipulate before it is sent. */ request: { /** - * Helpers for manipulating the request headers. + * The headers of the request. */ - headers: { - /** - * Add one header to the request. - * - * @param header - The name of the header. - * @param value - The value of the header. - */ - add: (header: string, value: string) => { - workerData.headers.append(header, value); - }, - /** - * Add multiple headers to the request. - * - * @param headersInit - The {@link HeadersInit} to add. - */ - addMany: (headersInit: HeadersInit) => { - Kit.Headers.appendInit(workerData.headers, headersInit); - }, - }, + headers: workerData.request.headers, }, /** * Mimics the `prompt` function in the browser, by sending a message to the main thread @@ -208,7 +194,9 @@ ${script}})()`; type: WorkerEvents.Outgoing.Event.result, // todo: We need to more precisely type environment value. Currently unknown. Why? environmentVariables: workerData.environmentVariables as any, - headers: Array.from(workerData.headers.entries()), + request: { + headers: Array.from(workerData.request.headers.entries()), + }, }); } diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index e3c123a3e5..7b76a5f9f5 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -119,7 +119,9 @@ export namespace WorkerEvents { export interface Result { type: Event.result; environmentVariables: Record; - headers: [name: string, value: string][]; + request: { + headers: [name: string, value: string][]; + }; } export interface Ready { diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 60bed5c8c0..8ae12e31a4 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -340,7 +340,7 @@ function LaboratoryPageContent(props: { // // What should really happen is that GraphiQLFetcher accepts a HeadersInit type. // - const newHeadersLossyFixMe = Object.fromEntries(result.headers); + const newHeadersLossyFixMe = Object.fromEntries(result.request.headers); headers = { ...headers, ...newHeadersLossyFixMe, From a21ea80aff90fa8fdbd8c73295f31a17009af717 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:33:32 -0500 Subject: [PATCH 05/45] finish refactor: connected event type defs --- .../src/lib/preflight-sandbox/shared-types.ts | 158 ++++++++++-------- 1 file changed, 88 insertions(+), 70 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index 7b76a5f9f5..f653a41999 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -13,46 +13,51 @@ export namespace IFrameEvents { prompt = 'prompt', } - type ReadyEventData = { - type: Event.ready; - }; - - type StartEventData = { - type: Event.start; - runId: string; - }; - - type LogEventData = { - type: Event.log; - runId: string; - log: string | Error; - }; - - export interface ResultEventData extends Omit { - type: Event.result; - runId: string; + export namespace EventData { + export interface ReadyEventData { + type: Event.ready; + } + + export interface StartEventData { + type: Event.start; + runId: string; + } + + export interface LogEventData { + type: Event.log; + runId: string; + log: string | Error; + } + + export interface ResultEventData + extends Omit { + type: Event.result; + runId: string; + } + + export interface ErrorEventData { + type: Event.error; + runId: string; + error: Error; + } + + export interface PromptEventData { + type: Event.prompt; + runId: string; + promptId: number; + message: string; + defaultValue?: string; + } } - type ErrorEventData = { - type: Event.error; - runId: string; - error: Error; - }; - - type PromptEventData = { - type: Event.prompt; - runId: string; - promptId: number; - message: string; - defaultValue?: string; - }; - - export type EventData = - | ReadyEventData - | StartEventData - | LogEventData - | PromptEventData - | ResultEventData - | ErrorEventData; + + export type EventData = { + ready: EventData.ReadyEventData; + start: EventData.StartEventData; + log: EventData.LogEventData; + prompt: EventData.PromptEventData; + result: EventData.ResultEventData; + error: EventData.ErrorEventData; + }[Event]; export type MessageEvent = _MessageEvent; } @@ -64,26 +69,33 @@ export namespace IFrameEvents { abort = 'abort', } - type RunEventData = { - type: Event.run; - id: string; - script: string; - environmentVariables: Record; - }; - - type AbortEventData = { - type: Event.abort; - id: string; - }; - - type PromptResponseEventData = { - type: Event.promptResponse; - id: string; - promptId: number; - value: string | null; - }; - - export type EventData = RunEventData | AbortEventData | PromptResponseEventData; + export namespace EventData { + export interface RunEventData { + type: Event.run; + id: string; + script: string; + environmentVariables: Record; + } + + export interface AbortEventData { + type: Event.abort; + id: string; + } + + export interface PromptResponseEventData { + type: Event.promptResponse; + id: string; + promptId: number; + value: string | null; + } + } + + export type EventData = { + run: EventData.RunEventData; + promptResponse: EventData.PromptResponseEventData; + abort: EventData.AbortEventData; + }[Event]; + export type MessageEvent = _MessageEvent; } } @@ -146,19 +158,25 @@ export namespace WorkerEvents { promptResponse = 'promptResponse', } - type PromptResponseEventData = { - type: Event.promptResponse; - promptId: number; - value: string | null; - }; + export namespace EventData { + export interface PromptResponseEventData { + type: Event.promptResponse; + promptId: number; + value: string | null; + } + + export interface RunEventData { + type: Event.run; + script: string; + environmentVariables: Record; + } + } - type RunEventData = { - type: Event.run; - script: string; - environmentVariables: Record; - }; + export type EventData = { + promptResponse: EventData.PromptResponseEventData; + run: EventData.RunEventData; + }[Event]; - export type EventData = PromptResponseEventData | RunEventData; export type MessageEvent = _MessageEvent; } } From cd12274ea7897d592f07f955e0ade2b5bc0f4633 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:38:48 -0500 Subject: [PATCH 06/45] no substitute headers preflight script --- packages/web/app/src/pages/target-laboratory.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 8ae12e31a4..4edefb1d65 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -328,10 +328,12 @@ function LaboratoryPageContent(props: { }); try { const result = await preflightScript.execute(); - // We merge the result headers into the fetcher headers before performing header variable substitution - // so that users can have a predictable experience working with headers regardless of the place those - // headers come from. + // We merge the result headers into the fetcher headers AFTER performing header variable substitution. + // This ensures users have a predictable standards-compliant experience working with headers in their + // preflight script. + // todo: add test case covering this case. // + headers = substituteVariablesInHeaders(headers, result.environmentVariables); // todo: GraphiQLFetcher appears to only support record-shaped headers which seems wrong because it // precludes complete usage of Headers data structure, namely where there are multiple values for one // header. We could try to hack a solution here by doing merges of such cases but that seems @@ -345,7 +347,6 @@ function LaboratoryPageContent(props: { ...headers, ...newHeadersLossyFixMe, }; - headers = substituteVariablesInHeaders(headers, result.environmentVariables); } catch (err: unknown) { if (err instanceof Error === false) { throw err; From fc2f8c931b0ad2b32859ee7dda70f31b33fa6da7 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:45:18 -0500 Subject: [PATCH 07/45] embrace headers limitation for now --- .../web/app/src/pages/target-laboratory.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 4edefb1d65..6a57f05ee4 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -334,18 +334,25 @@ function LaboratoryPageContent(props: { // todo: add test case covering this case. // headers = substituteVariablesInHeaders(headers, result.environmentVariables); - // todo: GraphiQLFetcher appears to only support record-shaped headers which seems wrong because it + // todo: test case showing this is a limitation. + // todo: website documentation mentioning this limitation to our users. + // todo: jsdoc on `lab` mentioning this limitation to our users. + // todo: https://github.com/graphql/graphiql/pull/3854 + // We have submitted a PR to GraphiQL to fix the issue described below. + // Once shipped, remove our lossy code below. + // + // GraphiQLFetcher only support record-shaped headers which // precludes complete usage of Headers data structure, namely where there are multiple values for one - // header. We could try to hack a solution here by doing merges of such cases but that seems + // header. + // + // We could try to hack a solution here by doing merges of such cases but that seems // likely to introduce more bugs given the different formats that different kinds of headers use to // delineate multiple values. // - // What should really happen is that GraphiQLFetcher accepts a HeadersInit type. - // - const newHeadersLossyFixMe = Object.fromEntries(result.request.headers); + const newHeadersLossy = Object.fromEntries(result.request.headers); headers = { ...headers, - ...newHeadersLossyFixMe, + ...newHeadersLossy, }; } catch (err: unknown) { if (err instanceof Error === false) { From b5f21b0b53686b3e22ad3cbd26d95bc6f7f0f9c3 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:49:21 -0500 Subject: [PATCH 08/45] no type-encoding of passthrough --- packages/web/app/src/lib/kit/headers.ts | 1 + .../web/app/src/lib/preflight-sandbox/shared-types.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/web/app/src/lib/kit/headers.ts b/packages/web/app/src/lib/kit/headers.ts index 00d8d56c7b..a9f26258a3 100644 --- a/packages/web/app/src/lib/kit/headers.ts +++ b/packages/web/app/src/lib/kit/headers.ts @@ -1,4 +1,5 @@ export namespace Headers { + export type Encoded = [name: string, value: string][]; /** * Take given HeadersInit and append it (mutating) into given Headers. * diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index f653a41999..10df269c82 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { Kit } from '../kit'; + type _MessageEvent = MessageEvent; export namespace IFrameEvents { @@ -29,10 +31,13 @@ export namespace IFrameEvents { log: string | Error; } - export interface ResultEventData - extends Omit { + export interface ResultEventData { type: Event.result; runId: string; + environmentVariables: Record; + request: { + headers: Kit.Headers.Encoded; + }; } export interface ErrorEventData { @@ -132,7 +137,7 @@ export namespace WorkerEvents { type: Event.result; environmentVariables: Record; request: { - headers: [name: string, value: string][]; + headers: Kit.Headers.Encoded; }; } From e24df4de959fa30238130944fb04c3afa2643b2c Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:53:48 -0500 Subject: [PATCH 09/45] fix --- .../lib/preflight-sandbox/graphiql-plugin.tsx | 11 +++-- .../src/lib/preflight-sandbox/shared-types.ts | 48 +++++++++---------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index f46f0d1223..5d3d08af0b 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -43,7 +43,7 @@ import { cn } from '../utils'; import type { LogMessage } from './preflight-script-worker'; import { IFrameEvents } from './shared-types'; -type Result = Omit; +type Result = Omit; export const preflightScriptPlugin: GraphiQLPlugin = { icon: () => ( @@ -190,12 +190,15 @@ export function usePreflightScript(args: { const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', '[]'); const latestHeadersRef = useRef(headers); useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore - const decodeHeaders = (encoded: string) => safeParseJSON(encoded) ?? []; + const decodeHeaders = (encoded: string) => + safeParseJSON(encoded) ?? []; const decodeResult = (): Result => { return { environmentVariables: decodeEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore - headers: decodeHeaders(latestHeadersRef.current), + request: { + headers: decodeHeaders(latestHeadersRef.current), + }, }; }; // ----------- @@ -295,7 +298,7 @@ export function usePreflightScript(args: { const mergedHeadersEncoded = Kit.JSON.encodePretty([ ...decodeHeaders(latestHeadersRef.current), - ...ev.data.headers, + ...ev.data.request.headers, ]); setHeaders(mergedHeadersEncoded); latestHeadersRef.current = mergedHeadersEncoded; diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index 10df269c82..7b000062f7 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -16,22 +16,22 @@ export namespace IFrameEvents { } export namespace EventData { - export interface ReadyEventData { + export interface Ready { type: Event.ready; } - export interface StartEventData { + export interface Start { type: Event.start; runId: string; } - export interface LogEventData { + export interface Log { type: Event.log; runId: string; - log: string | Error; + log: string | globalThis.Error; } - export interface ResultEventData { + export interface Result { type: Event.result; runId: string; environmentVariables: Record; @@ -40,13 +40,13 @@ export namespace IFrameEvents { }; } - export interface ErrorEventData { + export interface Error { type: Event.error; runId: string; - error: Error; + error: globalThis.Error; } - export interface PromptEventData { + export interface Prompt { type: Event.prompt; runId: string; promptId: number; @@ -56,12 +56,12 @@ export namespace IFrameEvents { } export type EventData = { - ready: EventData.ReadyEventData; - start: EventData.StartEventData; - log: EventData.LogEventData; - prompt: EventData.PromptEventData; - result: EventData.ResultEventData; - error: EventData.ErrorEventData; + ready: EventData.Ready; + start: EventData.Start; + log: EventData.Log; + prompt: EventData.Prompt; + result: EventData.Result; + error: EventData.Error; }[Event]; export type MessageEvent = _MessageEvent; @@ -75,19 +75,19 @@ export namespace IFrameEvents { } export namespace EventData { - export interface RunEventData { + export interface Run { type: Event.run; id: string; script: string; environmentVariables: Record; } - export interface AbortEventData { + export interface Abort { type: Event.abort; id: string; } - export interface PromptResponseEventData { + export interface PromptResponse { type: Event.promptResponse; id: string; promptId: number; @@ -96,9 +96,9 @@ export namespace IFrameEvents { } export type EventData = { - run: EventData.RunEventData; - promptResponse: EventData.PromptResponseEventData; - abort: EventData.AbortEventData; + run: EventData.Run; + promptResponse: EventData.PromptResponse; + abort: EventData.Abort; }[Event]; export type MessageEvent = _MessageEvent; @@ -164,13 +164,13 @@ export namespace WorkerEvents { } export namespace EventData { - export interface PromptResponseEventData { + export interface PromptResponse { type: Event.promptResponse; promptId: number; value: string | null; } - export interface RunEventData { + export interface Run { type: Event.run; script: string; environmentVariables: Record; @@ -178,8 +178,8 @@ export namespace WorkerEvents { } export type EventData = { - promptResponse: EventData.PromptResponseEventData; - run: EventData.RunEventData; + promptResponse: EventData.PromptResponse; + run: EventData.Run; }[Event]; export type MessageEvent = _MessageEvent; From fbe7eee87b0aeb7b23595731f168c5345176b4f9 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 09:58:40 -0500 Subject: [PATCH 10/45] mention todo --- .../web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 5d3d08af0b..26746d04df 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -181,6 +181,13 @@ export function usePreflightScript(args: { // todo: Improve `useLocalStorage` by allowing passing a codec? Then we can co-locate // the codec with the data and have it applied transparently, use a decoded value // for the default, etc. ? + // + // todo: Stop swallowing decode errors (we return null on parse failure), monitor them. + // If JSON parsing fails, it should only be because a stored value was actually invalid. + // However given these values also include userland interaction (e.g. user could muck around + // in their browser local storage) we would need to apply an appropriate filter on incoming + // errors such as error rate analysis and only keep the most clearly egregious signals + // for off-work alerting. const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', '{}'); // prettier-ignore const latestEnvironmentVariablesRef = useRef(environmentVariables); From 998c566ddcc303c3a8fc1dabd41563eaea2f326e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 11:32:49 -0500 Subject: [PATCH 11/45] make noop cases debuggable --- packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 26746d04df..c1d6ac44f8 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -344,10 +344,12 @@ export function usePreflightScript(args: { } if (ev.data.type === IFrameEvents.Outgoing.Event.ready) { + console.debug('preflight sandbox graphiql plugin: noop iframe event:', ev.data); return; } if (ev.data.type === IFrameEvents.Outgoing.Event.start) { + console.debug('preflight sandbox graphiql plugin: noop iframe event:', ev.data); return; } From 28b8bbb72a072c01552cd54d72f410ee66ddd6c9 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 20 Jan 2025 11:56:37 -0500 Subject: [PATCH 12/45] kit json decode safe --- packages/web/app/src/lib/kit/types/json.ts | 23 ++++++--- .../lib/preflight-sandbox/graphiql-plugin.tsx | 48 +++++++++---------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/packages/web/app/src/lib/kit/types/json.ts b/packages/web/app/src/lib/kit/types/json.ts index d8962d0dd2..1bb7308f6c 100644 --- a/packages/web/app/src/lib/kit/types/json.ts +++ b/packages/web/app/src/lib/kit/types/json.ts @@ -1,10 +1,12 @@ export namespace JSON { - export const encode = (value: value): string => { - return globalThis.JSON.stringify(value); - }; - - export const encodePretty = (value: value): string => { - return globalThis.JSON.stringify(value, null, 2); + export const decodeSafe = <$UnsafeCast extends Value = Value>( + encodedValue: string, + ): $UnsafeCast | SyntaxError => { + try { + return decode(encodedValue) as $UnsafeCast; + } catch (error) { + return error as SyntaxError; + } }; export const decode = (value: string): Value => { @@ -16,4 +18,13 @@ export namespace JSON { export type NonPrimitiveValue = { [key: string]: Value } | Array; export type PrimitiveValue = string | number | boolean | null; + + // If team wants symmetric code across encoding/decoding of JSON, we can use this: + // export const encode = (value: value): string => { + // return globalThis.JSON.stringify(value); + // }; + + // export const encodePretty = (value: value): string => { + // return globalThis.JSON.stringify(value, null, 2); + // }; } diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index c1d6ac44f8..10f8b816b7 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -144,16 +144,6 @@ const PreflightScript_TargetFragment = graphql(` type LogRecord = LogMessage | { type: 'separator' }; -function safeParseJSON<$UnsafeCast extends Kit.JSON.Value = Kit.JSON.Value>( - str: string, -): $UnsafeCast | null { - try { - return Kit.JSON.decode(str) as $UnsafeCast; - } catch { - return null; - } -} - const enum PreflightWorkerState { running, ready, @@ -192,19 +182,24 @@ export function usePreflightScript(args: { const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', '{}'); // prettier-ignore const latestEnvironmentVariablesRef = useRef(environmentVariables); useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore - const decodeEnvironmentVariables = (encoded: string) => safeParseJSON(encoded) ?? {}; // prettier-ignore + const decodeResultEnvironmentVariables = (encoded: string) => { + const result = Kit.JSON.decodeSafe(encoded); + return result instanceof SyntaxError ? {} : result; + }; const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', '[]'); const latestHeadersRef = useRef(headers); useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore - const decodeHeaders = (encoded: string) => - safeParseJSON(encoded) ?? []; + const decodeResultHeaders = (encoded: string) => { + const result = Kit.JSON.decodeSafe(encoded); + return result instanceof SyntaxError ? [] : result; + }; const decodeResult = (): Result => { return { - environmentVariables: decodeEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore + environmentVariables: decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore request: { - headers: decodeHeaders(latestHeadersRef.current), + headers: decodeResultHeaders(latestHeadersRef.current), }, }; }; @@ -240,7 +235,7 @@ export function usePreflightScript(args: { type: IFrameEvents.Incoming.Event.run, id, script, - environmentVariables: decodeEnvironmentVariables(environmentVariables), + environmentVariables: decodeResultEnvironmentVariables(environmentVariables), } satisfies IFrameEvents.Incoming.EventData, '*', ); @@ -296,17 +291,22 @@ export function usePreflightScript(args: { } if (ev.data.type === IFrameEvents.Outgoing.Event.result) { - const mergedEnvironmentVariablesEncoded = Kit.JSON.encodePretty({ - ...decodeEnvironmentVariables(latestEnvironmentVariablesRef.current), - ...ev.data.environmentVariables, - }); + const mergedEnvironmentVariablesEncoded = JSON.stringify( + { + ...decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), + ...ev.data.environmentVariables, + }, + null, + 2, + ); setEnvironmentVariables(mergedEnvironmentVariablesEncoded); latestEnvironmentVariablesRef.current = mergedEnvironmentVariablesEncoded; - const mergedHeadersEncoded = Kit.JSON.encodePretty([ - ...decodeHeaders(latestHeadersRef.current), - ...ev.data.request.headers, - ]); + const mergedHeadersEncoded = JSON.stringify( + [...decodeResultHeaders(latestHeadersRef.current), ...ev.data.request.headers], + null, + 2, + ); setHeaders(mergedHeadersEncoded); latestHeadersRef.current = mergedHeadersEncoded; From 557bc4ebcc1da745bc056ec17b6f6c0584073dad Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 09:16:31 -0500 Subject: [PATCH 13/45] wip --- .../lib/preflight-sandbox/graphiql-plugin.tsx | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 10f8b816b7..1f7f9bb502 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -169,8 +169,7 @@ export function usePreflightScript(args: { // Use a proper versioned schema with codecs for coercing, defaults, validation, etc. // // todo: Improve `useLocalStorage` by allowing passing a codec? Then we can co-locate - // the codec with the data and have it applied transparently, use a decoded value - // for the default, etc. ? + // the codec with the data and have it applied transparently // // todo: Stop swallowing decode errors (we return null on parse failure), monitor them. // If JSON parsing fails, it should only be because a stored value was actually invalid. @@ -179,15 +178,15 @@ export function usePreflightScript(args: { // errors such as error rate analysis and only keep the most clearly egregious signals // for off-work alerting. - const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', '{}'); // prettier-ignore + const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', {} as Result['environmentVariables']); // prettier-ignore const latestEnvironmentVariablesRef = useRef(environmentVariables); useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore - const decodeResultEnvironmentVariables = (encoded: string) => { - const result = Kit.JSON.decodeSafe(encoded); - return result instanceof SyntaxError ? {} : result; - }; + // const decodeResultEnvironmentVariables = (encoded: string) => { + // const result = Kit.JSON.decodeSafe(encoded); + // return result instanceof SyntaxError ? {} : result; + // }; - const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', '[]'); + const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', [] as Result['request']['headers']); // prettier-ignore const latestHeadersRef = useRef(headers); useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore const decodeResultHeaders = (encoded: string) => { @@ -197,9 +196,9 @@ export function usePreflightScript(args: { const decodeResult = (): Result => { return { - environmentVariables: decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore + environmentVariables: latestEnvironmentVariablesRef.current, request: { - headers: decodeResultHeaders(latestHeadersRef.current), + headers: latestHeadersRef.current, }, }; }; @@ -235,7 +234,7 @@ export function usePreflightScript(args: { type: IFrameEvents.Incoming.Event.run, id, script, - environmentVariables: decodeResultEnvironmentVariables(environmentVariables), + environmentVariables, } satisfies IFrameEvents.Incoming.EventData, '*', ); @@ -291,24 +290,16 @@ export function usePreflightScript(args: { } if (ev.data.type === IFrameEvents.Outgoing.Event.result) { - const mergedEnvironmentVariablesEncoded = JSON.stringify( - { - ...decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), - ...ev.data.environmentVariables, - }, - null, - 2, - ); - setEnvironmentVariables(mergedEnvironmentVariablesEncoded); - latestEnvironmentVariablesRef.current = mergedEnvironmentVariablesEncoded; - - const mergedHeadersEncoded = JSON.stringify( - [...decodeResultHeaders(latestHeadersRef.current), ...ev.data.request.headers], - null, - 2, - ); - setHeaders(mergedHeadersEncoded); - latestHeadersRef.current = mergedHeadersEncoded; + const mergedEnvironmentVariables = { + ...latestEnvironmentVariablesRef.current, + ...ev.data.environmentVariables, + }; + setEnvironmentVariables(mergedEnvironmentVariables); + latestEnvironmentVariablesRef.current = mergedEnvironmentVariables; + + const mergedHeaders = [...latestHeadersRef.current, ...ev.data.request.headers]; + setHeaders(mergedHeaders); + latestHeadersRef.current = mergedHeaders; setLogs(logs => [ ...logs, @@ -408,8 +399,10 @@ export function usePreflightScript(args: { isPreflightScriptEnabled, setIsPreflightScriptEnabled, script: target?.preflightScript?.sourceCode ?? '', - environmentVariables, - setEnvironmentVariables, + data: { + environmentVariables, + setEnvironmentVariables, + }, state, logs, clearLogs: () => setLogs([]), @@ -493,8 +486,8 @@ function PreflightScriptContent() { logs={preflightScript.logs} clearLogs={preflightScript.clearLogs} onScriptValueChange={handleScriptChange} - envValue={preflightScript.environmentVariables} - onEnvValueChange={preflightScript.setEnvironmentVariables} + envValue={JSON.stringify(preflightScript.data.environmentVariables, null, 2)} + onEnvValueChange={value => preflightScript.data.setEnvironmentVariables(JSON.parse(value))} />
Preflight Script @@ -575,8 +568,10 @@ function PreflightScriptContent() { preflightScript.setEnvironmentVariables(value ?? '')} + value={JSON.stringify(preflightScript.data.environmentVariables, null, 2)} + onChange={value => + preflightScript.data.setEnvironmentVariables(value ? JSON.parse(value) : {}) + } {...monacoProps.env} className={classes.monacoMini} wrapperProps={{ From 52222f020f6a0720d0a4c8e01bee4a6c8d5e3cfa Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 11:22:33 -0500 Subject: [PATCH 14/45] wip --- .../lib/preflight-sandbox/graphiql-plugin.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 6d649ab659..ad2527d84e 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -62,7 +62,7 @@ export const preflightScriptPlugin: GraphiQLPlugin = { ), title: 'Preflight Script', - content: PreflightScriptContent, + content: preflightScriptPluginContent, }; const classes = { @@ -180,6 +180,7 @@ export function usePreflightScript(args: { // for off-work alerting. const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', {} as Result['environmentVariables']); // prettier-ignore + console.log('useLocalStorage', { environmentVariables }); const latestEnvironmentVariablesRef = useRef(environmentVariables); useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore // const decodeResultEnvironmentVariables = (encoded: string) => { @@ -188,12 +189,13 @@ export function usePreflightScript(args: { // }; const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', [] as Result['request']['headers']); // prettier-ignore + console.log('useLocalStorage', { headers }); const latestHeadersRef = useRef(headers); useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore - const decodeResultHeaders = (encoded: string) => { - const result = Kit.JSON.decodeSafe(encoded); - return result instanceof SyntaxError ? [] : result; - }; + // const decodeResultHeaders = (encoded: string) => { + // const result = Kit.JSON.decodeSafe(encoded); + // return result instanceof SyntaxError ? [] : result; + // }; const decodeResult = (): Result => { return { @@ -424,12 +426,13 @@ export function usePreflightScript(args: { } as const; } -type PreflightScriptObject = ReturnType; +type PreflightScript = ReturnType; -const PreflightScriptContext = createContext(null); +const PreflightScriptContext = createContext(null); export const PreflightScriptProvider = PreflightScriptContext.Provider; -function PreflightScriptContent() { +// todo: Move toward module top +function preflightScriptPluginContent() { const preflightScript = useContext(PreflightScriptContext); if (preflightScript === null) { throw new Error('PreflightScriptContent used outside PreflightScriptContext.Provider'); @@ -570,9 +573,25 @@ function PreflightScriptContent() { - preflightScript.data.setEnvironmentVariables(value ? JSON.parse(value) : {}) - } + onChange={content => { + const contentTrimmed = content?.trim(); + const contentIsEmpty = !contentTrimmed; + + if (contentIsEmpty) { + console.debug('environment variables editor: change: empty -> clear'); + preflightScript.data.setEnvironmentVariables({}); + return; + } + + const valueDecoded = Kit.JSON.decodeSafe>(contentTrimmed); + if (valueDecoded instanceof SyntaxError) { + console.debug('environment variables editor: change: invalid JSON -> ignore'); + return; + } + + console.debug('environment variables editor: change: OK'); + preflightScript.data.setEnvironmentVariables(valueDecoded); + }} {...monacoProps.env} className={classes.monacoMini} wrapperProps={{ From 1212fc83ec1fd6a2abf6ad4e7b56f22384a0372d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 12:17:40 -0500 Subject: [PATCH 15/45] Revert "wip" This reverts commit 52222f020f6a0720d0a4c8e01bee4a6c8d5e3cfa. --- .../lib/preflight-sandbox/graphiql-plugin.tsx | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index ad2527d84e..6d649ab659 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -62,7 +62,7 @@ export const preflightScriptPlugin: GraphiQLPlugin = { ), title: 'Preflight Script', - content: preflightScriptPluginContent, + content: PreflightScriptContent, }; const classes = { @@ -180,7 +180,6 @@ export function usePreflightScript(args: { // for off-work alerting. const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', {} as Result['environmentVariables']); // prettier-ignore - console.log('useLocalStorage', { environmentVariables }); const latestEnvironmentVariablesRef = useRef(environmentVariables); useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore // const decodeResultEnvironmentVariables = (encoded: string) => { @@ -189,13 +188,12 @@ export function usePreflightScript(args: { // }; const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', [] as Result['request']['headers']); // prettier-ignore - console.log('useLocalStorage', { headers }); const latestHeadersRef = useRef(headers); useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore - // const decodeResultHeaders = (encoded: string) => { - // const result = Kit.JSON.decodeSafe(encoded); - // return result instanceof SyntaxError ? [] : result; - // }; + const decodeResultHeaders = (encoded: string) => { + const result = Kit.JSON.decodeSafe(encoded); + return result instanceof SyntaxError ? [] : result; + }; const decodeResult = (): Result => { return { @@ -426,13 +424,12 @@ export function usePreflightScript(args: { } as const; } -type PreflightScript = ReturnType; +type PreflightScriptObject = ReturnType; -const PreflightScriptContext = createContext(null); +const PreflightScriptContext = createContext(null); export const PreflightScriptProvider = PreflightScriptContext.Provider; -// todo: Move toward module top -function preflightScriptPluginContent() { +function PreflightScriptContent() { const preflightScript = useContext(PreflightScriptContext); if (preflightScript === null) { throw new Error('PreflightScriptContent used outside PreflightScriptContext.Provider'); @@ -573,25 +570,9 @@ function preflightScriptPluginContent() { { - const contentTrimmed = content?.trim(); - const contentIsEmpty = !contentTrimmed; - - if (contentIsEmpty) { - console.debug('environment variables editor: change: empty -> clear'); - preflightScript.data.setEnvironmentVariables({}); - return; - } - - const valueDecoded = Kit.JSON.decodeSafe>(contentTrimmed); - if (valueDecoded instanceof SyntaxError) { - console.debug('environment variables editor: change: invalid JSON -> ignore'); - return; - } - - console.debug('environment variables editor: change: OK'); - preflightScript.data.setEnvironmentVariables(valueDecoded); - }} + onChange={value => + preflightScript.data.setEnvironmentVariables(value ? JSON.parse(value) : {}) + } {...monacoProps.env} className={classes.monacoMini} wrapperProps={{ From c890264d5342329ecc0e85b6b71c82cd7fdd8408 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 12:17:52 -0500 Subject: [PATCH 16/45] Revert "wip" This reverts commit 557bc4ebcc1da745bc056ec17b6f6c0584073dad. --- .../lib/preflight-sandbox/graphiql-plugin.tsx | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 6d649ab659..1c47fd2a2d 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -170,7 +170,8 @@ export function usePreflightScript(args: { // Use a proper versioned schema with codecs for coercing, defaults, validation, etc. // // todo: Improve `useLocalStorage` by allowing passing a codec? Then we can co-locate - // the codec with the data and have it applied transparently + // the codec with the data and have it applied transparently, use a decoded value + // for the default, etc. ? // // todo: Stop swallowing decode errors (we return null on parse failure), monitor them. // If JSON parsing fails, it should only be because a stored value was actually invalid. @@ -179,15 +180,15 @@ export function usePreflightScript(args: { // errors such as error rate analysis and only keep the most clearly egregious signals // for off-work alerting. - const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', {} as Result['environmentVariables']); // prettier-ignore + const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', '{}'); // prettier-ignore const latestEnvironmentVariablesRef = useRef(environmentVariables); useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore - // const decodeResultEnvironmentVariables = (encoded: string) => { - // const result = Kit.JSON.decodeSafe(encoded); - // return result instanceof SyntaxError ? {} : result; - // }; + const decodeResultEnvironmentVariables = (encoded: string) => { + const result = Kit.JSON.decodeSafe(encoded); + return result instanceof SyntaxError ? {} : result; + }; - const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', [] as Result['request']['headers']); // prettier-ignore + const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', '[]'); const latestHeadersRef = useRef(headers); useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore const decodeResultHeaders = (encoded: string) => { @@ -197,9 +198,9 @@ export function usePreflightScript(args: { const decodeResult = (): Result => { return { - environmentVariables: latestEnvironmentVariablesRef.current, + environmentVariables: decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore request: { - headers: latestHeadersRef.current, + headers: decodeResultHeaders(latestHeadersRef.current), }, }; }; @@ -235,7 +236,7 @@ export function usePreflightScript(args: { type: IFrameEvents.Incoming.Event.run, id, script, - environmentVariables, + environmentVariables: decodeResultEnvironmentVariables(environmentVariables), } satisfies IFrameEvents.Incoming.EventData, '*', ); @@ -291,16 +292,24 @@ export function usePreflightScript(args: { } if (ev.data.type === IFrameEvents.Outgoing.Event.result) { - const mergedEnvironmentVariables = { - ...latestEnvironmentVariablesRef.current, - ...ev.data.environmentVariables, - }; - setEnvironmentVariables(mergedEnvironmentVariables); - latestEnvironmentVariablesRef.current = mergedEnvironmentVariables; - - const mergedHeaders = [...latestHeadersRef.current, ...ev.data.request.headers]; - setHeaders(mergedHeaders); - latestHeadersRef.current = mergedHeaders; + const mergedEnvironmentVariablesEncoded = JSON.stringify( + { + ...decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), + ...ev.data.environmentVariables, + }, + null, + 2, + ); + setEnvironmentVariables(mergedEnvironmentVariablesEncoded); + latestEnvironmentVariablesRef.current = mergedEnvironmentVariablesEncoded; + + const mergedHeadersEncoded = JSON.stringify( + [...decodeResultHeaders(latestHeadersRef.current), ...ev.data.request.headers], + null, + 2, + ); + setHeaders(mergedHeadersEncoded); + latestHeadersRef.current = mergedHeadersEncoded; setLogs(logs => [ ...logs, @@ -400,10 +409,8 @@ export function usePreflightScript(args: { isPreflightScriptEnabled, setIsPreflightScriptEnabled, script: target?.preflightScript?.sourceCode ?? '', - data: { - environmentVariables, - setEnvironmentVariables, - }, + environmentVariables, + setEnvironmentVariables, state, logs, clearLogs: () => setLogs([]), @@ -487,8 +494,8 @@ function PreflightScriptContent() { logs={preflightScript.logs} clearLogs={preflightScript.clearLogs} onScriptValueChange={handleScriptChange} - envValue={JSON.stringify(preflightScript.data.environmentVariables, null, 2)} - onEnvValueChange={value => preflightScript.data.setEnvironmentVariables(JSON.parse(value))} + envValue={preflightScript.environmentVariables} + onEnvValueChange={preflightScript.setEnvironmentVariables} />
Preflight Script @@ -569,10 +576,8 @@ function PreflightScriptContent() { - preflightScript.data.setEnvironmentVariables(value ? JSON.parse(value) : {}) - } + value={preflightScript.environmentVariables} + onChange={value => preflightScript.setEnvironmentVariables(value ?? '')} {...monacoProps.env} className={classes.monacoMini} wrapperProps={{ From 04a96fd986d81e42c12f77095e1691593ecc57c8 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 12:50:39 -0500 Subject: [PATCH 17/45] work --- .../lib/preflight-sandbox/graphiql-plugin.tsx | 74 ++++++------------- 1 file changed, 21 insertions(+), 53 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 1c47fd2a2d..f75fce1dcd 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -162,25 +162,7 @@ export function usePreflightScript(args: { false, ); - // ------------ - // Result State - // ------------ - // - // todo: Probably better to store the result as a single JSON object value. - // Use a proper versioned schema with codecs for coercing, defaults, validation, etc. - // - // todo: Improve `useLocalStorage` by allowing passing a codec? Then we can co-locate - // the codec with the data and have it applied transparently, use a decoded value - // for the default, etc. ? - // - // todo: Stop swallowing decode errors (we return null on parse failure), monitor them. - // If JSON parsing fails, it should only be because a stored value was actually invalid. - // However given these values also include userland interaction (e.g. user could muck around - // in their browser local storage) we would need to apply an appropriate filter on incoming - // errors such as error rate analysis and only keep the most clearly egregious signals - // for off-work alerting. - - const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', '{}'); // prettier-ignore + const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', ''); // prettier-ignore const latestEnvironmentVariablesRef = useRef(environmentVariables); useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore const decodeResultEnvironmentVariables = (encoded: string) => { @@ -188,24 +170,6 @@ export function usePreflightScript(args: { return result instanceof SyntaxError ? {} : result; }; - const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', '[]'); - const latestHeadersRef = useRef(headers); - useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore - const decodeResultHeaders = (encoded: string) => { - const result = Kit.JSON.decodeSafe(encoded); - return result instanceof SyntaxError ? [] : result; - }; - - const decodeResult = (): Result => { - return { - environmentVariables: decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore - request: { - headers: decodeResultHeaders(latestHeadersRef.current), - }, - }; - }; - // ----------- - const [state, setState] = useState(PreflightWorkerState.ready); const [logs, setLogs] = useState([]); @@ -215,8 +179,15 @@ export function usePreflightScript(args: { script = target?.preflightScript?.sourceCode ?? '', isPreview = false, ): Promise { + const result: Result = { + environmentVariables: decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), + request: { + headers: [], + }, + }; + if (isPreview === false && !isPreflightScriptEnabled) { - return decodeResult(); + return result; } const id = crypto.randomUUID(); @@ -292,25 +263,22 @@ export function usePreflightScript(args: { } if (ev.data.type === IFrameEvents.Outgoing.Event.result) { + const mergedEnvironmentVariables = { + ...decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), + ...ev.data.environmentVariables, + }; + result.environmentVariables = mergedEnvironmentVariables; + result.request.headers = ev.data.request.headers; + + // Write the new state of environment variables back to local storage. const mergedEnvironmentVariablesEncoded = JSON.stringify( - { - ...decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), - ...ev.data.environmentVariables, - }, + result.environmentVariables, null, 2, ); setEnvironmentVariables(mergedEnvironmentVariablesEncoded); latestEnvironmentVariablesRef.current = mergedEnvironmentVariablesEncoded; - const mergedHeadersEncoded = JSON.stringify( - [...decodeResultHeaders(latestHeadersRef.current), ...ev.data.request.headers], - null, - 2, - ); - setHeaders(mergedHeadersEncoded); - latestHeadersRef.current = mergedHeadersEncoded; - setLogs(logs => [ ...logs, `> End running script. Done in ${(Date.now() - now) / 1000}s`, @@ -334,7 +302,6 @@ export function usePreflightScript(args: { ]); setFinished(); closedOpenedPrompts(); - return; } @@ -373,7 +340,8 @@ export function usePreflightScript(args: { window.removeEventListener('message', eventHandler); setState(PreflightWorkerState.ready); - return decodeResult(); + + return result; } catch (err) { if (err instanceof Error) { setLogs(prev => [ @@ -385,7 +353,7 @@ export function usePreflightScript(args: { }, ]); setState(PreflightWorkerState.ready); - return decodeResult(); + return result; } throw err; } From 0b79e9f0136c834f042215313d9358d2ab1a4521 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 12:56:07 -0500 Subject: [PATCH 18/45] simplify --- .../app/src/lib/preflight-sandbox/preflight-worker-embed.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts index 51c29c3ead..d49a5fa98b 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts @@ -104,9 +104,10 @@ function handleEvent(data: IFrameEvents.Incoming.EventData) { if (ev.data.type === WorkerEvents.Outgoing.Event.result) { postMessage({ - ...ev.data, type: IFrameEvents.Outgoing.Event.result, runId, + environmentVariables: ev.data.environmentVariables, + request: ev.data.request, }); terminate(); return; From 09f15c6fbeb4649aeb612c6f36bbb7286b6067f2 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 13:28:29 -0500 Subject: [PATCH 19/45] refactor --- .../lib/preflight-sandbox/graphiql-plugin.tsx | 11 ++-- .../web/app/src/pages/target-laboratory.tsx | 54 +++++++++---------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index f75fce1dcd..46dc188617 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -44,7 +44,10 @@ import labApiDefinitionRaw from './lab-api-declaration?raw'; import type { LogMessage } from './preflight-script-worker'; import { IFrameEvents } from './shared-types'; -type Result = Omit; +export type PreflightScriptResultData = Omit< + IFrameEvents.Outgoing.EventData.Result, + 'type' | 'runId' +>; export const preflightScriptPlugin: GraphiQLPlugin = { icon: () => ( @@ -166,7 +169,7 @@ export function usePreflightScript(args: { const latestEnvironmentVariablesRef = useRef(environmentVariables); useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore const decodeResultEnvironmentVariables = (encoded: string) => { - const result = Kit.JSON.decodeSafe(encoded); + const result = Kit.JSON.decodeSafe(encoded); return result instanceof SyntaxError ? {} : result; }; @@ -178,8 +181,8 @@ export function usePreflightScript(args: { async function execute( script = target?.preflightScript?.sourceCode ?? '', isPreview = false, - ): Promise { - const result: Result = { + ): Promise { + const result: PreflightScriptResultData = { environmentVariables: decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), request: { headers: [], diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 6a57f05ee4..9244820cee 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -36,6 +36,7 @@ import { useResetState } from '@/lib/hooks/use-reset-state'; import { preflightScriptPlugin, PreflightScriptProvider, + PreflightScriptResultData, usePreflightScript, } from '@/lib/preflight-sandbox/graphiql-plugin'; import { cn } from '@/lib/utils'; @@ -313,7 +314,6 @@ function LaboratoryPageContent(props: { const fetcher = useMemo(() => { return async (params, opts) => { - let headers = opts?.headers ?? {}; const url = (actualSelectedApiEndpoint === 'linkedApi' ? target?.graphqlEndpointUrl : undefined) ?? mockEndpoint; @@ -326,34 +326,10 @@ function LaboratoryPageContent(props: { preflightScript.abort(); } }); + + let preflightData: PreflightScriptResultData; try { - const result = await preflightScript.execute(); - // We merge the result headers into the fetcher headers AFTER performing header variable substitution. - // This ensures users have a predictable standards-compliant experience working with headers in their - // preflight script. - // todo: add test case covering this case. - // - headers = substituteVariablesInHeaders(headers, result.environmentVariables); - // todo: test case showing this is a limitation. - // todo: website documentation mentioning this limitation to our users. - // todo: jsdoc on `lab` mentioning this limitation to our users. - // todo: https://github.com/graphql/graphiql/pull/3854 - // We have submitted a PR to GraphiQL to fix the issue described below. - // Once shipped, remove our lossy code below. - // - // GraphiQLFetcher only support record-shaped headers which - // precludes complete usage of Headers data structure, namely where there are multiple values for one - // header. - // - // We could try to hack a solution here by doing merges of such cases but that seems - // likely to introduce more bugs given the different formats that different kinds of headers use to - // delineate multiple values. - // - const newHeadersLossy = Object.fromEntries(result.request.headers); - headers = { - ...headers, - ...newHeadersLossy, - }; + preflightData = await preflightScript.execute(); } catch (err: unknown) { if (err instanceof Error === false) { throw err; @@ -375,6 +351,28 @@ function LaboratoryPageContent(props: { hasFinishedPreflightScript = true; } + const headers = { + // todo: test case covering point below + // We want to prevent users from interpolating environment variables into + // their preflight script headers. So, apply substitution BEFORE merging + // in preflight headers. + // + ...substituteVariablesInHeaders(opts?.headers ?? {}, preflightData.environmentVariables), + + // todo: test case covering this limitation + // todo: website documentation mentioning this limitation to our users. + // todo: jsdoc on `lab` mentioning this limitation to our users. + // todo: https://github.com/graphql/graphiql/pull/3854 + // We have submitted a PR to GraphiQL to fix the issue described below. + // Once shipped, remove our lossy code below. + // + // GraphiQLFetcher only support record-shaped headers which + // precludes complete usage of Headers data structure, namely where there are multiple values for one + // header. + // + ...Object.fromEntries(preflightData.request.headers), + }; + const graphiqlFetcher = createGraphiQLFetcher({ url, fetch }); const result = await graphiqlFetcher(params, { ...opts, From e3c253b72924c1e69d168da531e3a840419be4b4 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 13:34:11 -0500 Subject: [PATCH 20/45] use const --- .../web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 46dc188617..7d3fb2176b 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -210,7 +210,8 @@ export function usePreflightScript(args: { type: IFrameEvents.Incoming.Event.run, id, script, - environmentVariables: decodeResultEnvironmentVariables(environmentVariables), + // Preflight Script has read/write relationship with environment variables. + environmentVariables: result.environmentVariables, } satisfies IFrameEvents.Incoming.EventData, '*', ); @@ -273,7 +274,8 @@ export function usePreflightScript(args: { result.environmentVariables = mergedEnvironmentVariables; result.request.headers = ev.data.request.headers; - // Write the new state of environment variables back to local storage. + // Cause the new state of environment variables to be + // written back to local storage. const mergedEnvironmentVariablesEncoded = JSON.stringify( result.environmentVariables, null, From 15b7666cf327238f06a1daa2150d5211784c445b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 13:37:20 -0500 Subject: [PATCH 21/45] use const --- packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 7d3fb2176b..e649457ce3 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -268,7 +268,7 @@ export function usePreflightScript(args: { if (ev.data.type === IFrameEvents.Outgoing.Event.result) { const mergedEnvironmentVariables = { - ...decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), + ...result.environmentVariables, ...ev.data.environmentVariables, }; result.environmentVariables = mergedEnvironmentVariables; From 2622f99a6921d7ae7c8a4dfc7294f880afe7822b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 13:39:44 -0500 Subject: [PATCH 22/45] reduce diff --- .../app/src/lib/preflight-sandbox/graphiql-plugin.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index e649457ce3..bff73fd77f 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -165,9 +165,14 @@ export function usePreflightScript(args: { false, ); - const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', ''); // prettier-ignore + const [environmentVariables, setEnvironmentVariables] = useLocalStorage( + 'hive:laboratory:environment', + '', + ); const latestEnvironmentVariablesRef = useRef(environmentVariables); - useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore + useEffect(() => { + latestEnvironmentVariablesRef.current = environmentVariables; + }); const decodeResultEnvironmentVariables = (encoded: string) => { const result = Kit.JSON.decodeSafe(encoded); return result instanceof SyntaxError ? {} : result; From 525cb4e7dd45fdbf143d7ab9338a21ebc6ca6ccf Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 13:50:56 -0500 Subject: [PATCH 23/45] refactor --- packages/web/app/src/lib/kit/helpers.ts | 10 ++++++++++ packages/web/app/src/lib/kit/index.ts | 1 + .../src/lib/preflight-sandbox/graphiql-plugin.tsx | 12 ++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 packages/web/app/src/lib/kit/helpers.ts diff --git a/packages/web/app/src/lib/kit/helpers.ts b/packages/web/app/src/lib/kit/helpers.ts new file mode 100644 index 0000000000..bf375d2e56 --- /dev/null +++ b/packages/web/app/src/lib/kit/helpers.ts @@ -0,0 +1,10 @@ +export const tryOr = <$PrimaryResult, $FallbackResult>( + fn: () => $PrimaryResult, + fallback: () => $FallbackResult, +): $PrimaryResult | $FallbackResult => { + try { + return fn(); + } catch { + return fallback(); + } +}; diff --git a/packages/web/app/src/lib/kit/index.ts b/packages/web/app/src/lib/kit/index.ts index a649ddba3e..c5048484f2 100644 --- a/packages/web/app/src/lib/kit/index.ts +++ b/packages/web/app/src/lib/kit/index.ts @@ -3,3 +3,4 @@ export * as Kit from './index'; export * from './never'; export * from './headers'; export * from './types/json'; +export * from './helpers'; diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index bff73fd77f..c8fc3802f4 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -164,7 +164,6 @@ export function usePreflightScript(args: { 'hive:laboratory:isPreflightScriptEnabled', false, ); - const [environmentVariables, setEnvironmentVariables] = useLocalStorage( 'hive:laboratory:environment', '', @@ -173,10 +172,6 @@ export function usePreflightScript(args: { useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); - const decodeResultEnvironmentVariables = (encoded: string) => { - const result = Kit.JSON.decodeSafe(encoded); - return result instanceof SyntaxError ? {} : result; - }; const [state, setState] = useState(PreflightWorkerState.ready); const [logs, setLogs] = useState([]); @@ -187,11 +182,16 @@ export function usePreflightScript(args: { script = target?.preflightScript?.sourceCode ?? '', isPreview = false, ): Promise { + const resultEnvironmentVariablesDecoded: PreflightScriptResultData['environmentVariables'] = + Kit.tryOr( + () => JSON.parse(latestEnvironmentVariablesRef.current), + () => ({}), + ); const result: PreflightScriptResultData = { - environmentVariables: decodeResultEnvironmentVariables(latestEnvironmentVariablesRef.current), request: { headers: [], }, + environmentVariables: resultEnvironmentVariablesDecoded, }; if (isPreview === false && !isPreflightScriptEnabled) { From 3541df7c3219fb6ad5aeaddef8494ef2464a0980 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 13:56:39 -0500 Subject: [PATCH 24/45] untodo --- .../src/lib/preflight-sandbox/preflight-script-worker.ts | 5 ++--- .../web/app/src/lib/preflight-sandbox/shared-types.ts | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts index 47b36534db..e75c6e6125 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -9,7 +9,7 @@ interface WorkerData { request: { headers: Headers; }; - environmentVariables: Record; + environmentVariables: Record; } export type LogMessage = string | Error; @@ -192,8 +192,7 @@ ${script}})()`; sendMessage({ type: WorkerEvents.Outgoing.Event.result, - // todo: We need to more precisely type environment value. Currently unknown. Why? - environmentVariables: workerData.environmentVariables as any, + environmentVariables: workerData.environmentVariables, request: { headers: Array.from(workerData.request.headers.entries()), }, diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index 7b000062f7..71490c2d10 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -34,7 +34,7 @@ export namespace IFrameEvents { export interface Result { type: Event.result; runId: string; - environmentVariables: Record; + environmentVariables: Record; request: { headers: Kit.Headers.Encoded; }; @@ -79,7 +79,7 @@ export namespace IFrameEvents { type: Event.run; id: string; script: string; - environmentVariables: Record; + environmentVariables: Record; } export interface Abort { @@ -135,7 +135,7 @@ export namespace WorkerEvents { export interface Result { type: Event.result; - environmentVariables: Record; + environmentVariables: Record; request: { headers: Kit.Headers.Encoded; }; @@ -173,7 +173,7 @@ export namespace WorkerEvents { export interface Run { type: Event.run; script: string; - environmentVariables: Record; + environmentVariables: Record; } } From dfce1c48194f44d8b2c5efd1d43c86162dc9f73b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 14:00:54 -0500 Subject: [PATCH 25/45] explicit string --- packages/web/app/src/pages/target-laboratory.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 9244820cee..04a4edfc16 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -55,6 +55,7 @@ import 'graphiql/style.css'; import '@graphiql/plugin-explorer/style.css'; import { PromptManager, PromptProvider } from '@/components/ui/prompt'; import { useRedirect } from '@/lib/access/common'; +import { Kit } from '@/lib/kit'; const explorer = explorerPlugin(); @@ -254,15 +255,15 @@ function Save(props: { function substituteVariablesInHeaders( headers: Record, - environmentVariables: Record, + environmentVariables: Record, ) { return Object.fromEntries( Object.entries(headers).map(([key, value]) => { if (typeof value === 'string') { // Replace all occurrences of `{{keyName}}` strings only if key exists in `environmentVariables` - value = value.replaceAll(/{{(?.*?)}}/g, (originalString, envKey) => { + value = value.replaceAll(/{{(?.*?)}}/g, (originalString, envKey: string) => { return Object.hasOwn(environmentVariables, envKey) - ? (environmentVariables[envKey] as string) + ? String(environmentVariables[envKey]) : originalString; }); } From c8a62a4d3e87c36ff48c8a86aa94f78e64eac53d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 14:10:53 -0500 Subject: [PATCH 26/45] remove unused code --- packages/web/app/src/lib/kit/headers.ts | 12 ----------- packages/web/app/src/lib/kit/types/json.ts | 23 ---------------------- 2 files changed, 35 deletions(-) diff --git a/packages/web/app/src/lib/kit/headers.ts b/packages/web/app/src/lib/kit/headers.ts index a9f26258a3..044edf4c87 100644 --- a/packages/web/app/src/lib/kit/headers.ts +++ b/packages/web/app/src/lib/kit/headers.ts @@ -1,15 +1,3 @@ export namespace Headers { export type Encoded = [name: string, value: string][]; - /** - * Take given HeadersInit and append it (mutating) into given Headers. - * - * @param headers - The Headers object to append to. - * @param headersInit - The HeadersInit object to append from. - */ - export const appendInit = (headers: Headers, headersInit: HeadersInit): void => { - const newHeaders = new globalThis.Headers(headersInit); - newHeaders.forEach((value, key) => { - headers.append(key, value); - }); - }; } diff --git a/packages/web/app/src/lib/kit/types/json.ts b/packages/web/app/src/lib/kit/types/json.ts index 1bb7308f6c..90f41c559b 100644 --- a/packages/web/app/src/lib/kit/types/json.ts +++ b/packages/web/app/src/lib/kit/types/json.ts @@ -1,30 +1,7 @@ export namespace JSON { - export const decodeSafe = <$UnsafeCast extends Value = Value>( - encodedValue: string, - ): $UnsafeCast | SyntaxError => { - try { - return decode(encodedValue) as $UnsafeCast; - } catch (error) { - return error as SyntaxError; - } - }; - - export const decode = (value: string): Value => { - return globalThis.JSON.parse(value); - }; - export type Value = PrimitiveValue | NonPrimitiveValue; export type NonPrimitiveValue = { [key: string]: Value } | Array; export type PrimitiveValue = string | number | boolean | null; - - // If team wants symmetric code across encoding/decoding of JSON, we can use this: - // export const encode = (value: value): string => { - // return globalThis.JSON.stringify(value); - // }; - - // export const encodePretty = (value: value): string => { - // return globalThis.JSON.stringify(value, null, 2); - // }; } From 8f36d86c902515feabe174604055aa0863f0172c Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 14:37:26 -0500 Subject: [PATCH 27/45] use existing json module --- packages/web/app/src/lib/kit/index.ts | 3 +-- packages/web/app/src/lib/kit/{ => types}/headers.ts | 0 packages/web/app/src/lib/kit/types/json.ts | 7 ------- .../src/lib/preflight-sandbox/preflight-script-worker.ts | 5 ++--- .../web/app/src/lib/preflight-sandbox/shared-types.ts | 9 +++++---- packages/web/app/src/pages/target-laboratory.tsx | 3 ++- 6 files changed, 10 insertions(+), 17 deletions(-) rename packages/web/app/src/lib/kit/{ => types}/headers.ts (100%) delete mode 100644 packages/web/app/src/lib/kit/types/json.ts diff --git a/packages/web/app/src/lib/kit/index.ts b/packages/web/app/src/lib/kit/index.ts index c5048484f2..b65599cad7 100644 --- a/packages/web/app/src/lib/kit/index.ts +++ b/packages/web/app/src/lib/kit/index.ts @@ -1,6 +1,5 @@ export * as Kit from './index'; export * from './never'; -export * from './headers'; -export * from './types/json'; +export * from './types/headers'; export * from './helpers'; diff --git a/packages/web/app/src/lib/kit/headers.ts b/packages/web/app/src/lib/kit/types/headers.ts similarity index 100% rename from packages/web/app/src/lib/kit/headers.ts rename to packages/web/app/src/lib/kit/types/headers.ts diff --git a/packages/web/app/src/lib/kit/types/json.ts b/packages/web/app/src/lib/kit/types/json.ts deleted file mode 100644 index 90f41c559b..0000000000 --- a/packages/web/app/src/lib/kit/types/json.ts +++ /dev/null @@ -1,7 +0,0 @@ -export namespace JSON { - export type Value = PrimitiveValue | NonPrimitiveValue; - - export type NonPrimitiveValue = { [key: string]: Value } | Array; - - export type PrimitiveValue = string | number | boolean | null; -} diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts index e75c6e6125..e1cab5aedc 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -1,15 +1,14 @@ import CryptoJS from 'crypto-js'; import CryptoJSPackageJson from 'crypto-js/package.json'; -import { Kit } from '../kit'; import { ALLOWED_GLOBALS } from './allowed-globals'; -import { isJSONPrimitive } from './json'; +import { isJSONPrimitive, JSONValue } from './json'; import { WorkerEvents } from './shared-types'; interface WorkerData { request: { headers: Headers; }; - environmentVariables: Record; + environmentVariables: Record; } export type LogMessage = string | Error; diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index 71490c2d10..cf8668029a 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { Kit } from '../kit'; +import { JSONValue } from './json'; type _MessageEvent = MessageEvent; @@ -34,7 +35,7 @@ export namespace IFrameEvents { export interface Result { type: Event.result; runId: string; - environmentVariables: Record; + environmentVariables: Record; request: { headers: Kit.Headers.Encoded; }; @@ -79,7 +80,7 @@ export namespace IFrameEvents { type: Event.run; id: string; script: string; - environmentVariables: Record; + environmentVariables: Record; } export interface Abort { @@ -135,7 +136,7 @@ export namespace WorkerEvents { export interface Result { type: Event.result; - environmentVariables: Record; + environmentVariables: Record; request: { headers: Kit.Headers.Encoded; }; @@ -173,7 +174,7 @@ export namespace WorkerEvents { export interface Run { type: Event.run; script: string; - environmentVariables: Record; + environmentVariables: Record; } } diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 4f4c4bba0c..926795712e 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -59,6 +59,7 @@ import '@graphiql/plugin-explorer/style.css'; import { PromptManager, PromptProvider } from '@/components/ui/prompt'; import { useRedirect } from '@/lib/access/common'; import { Kit } from '@/lib/kit'; +import { JSONValue } from '@/lib/preflight-sandbox/json'; import { captureException } from '@sentry/react'; const explorer = explorerPlugin(); @@ -259,7 +260,7 @@ function Save(props: { function substituteVariablesInHeaders( headers: Record, - environmentVariables: Record, + environmentVariables: Record, ) { return Object.fromEntries( Object.entries(headers).map(([key, value]) => { From f80602797c5f371baabfbfdfa93350777ad6ce5a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 14:44:43 -0500 Subject: [PATCH 28/45] fix: json primitive value --- .../lib/preflight-sandbox/preflight-script-worker.ts | 4 ++-- .../web/app/src/lib/preflight-sandbox/shared-types.ts | 10 +++++----- packages/web/app/src/pages/target-laboratory.tsx | 5 ++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts index e1cab5aedc..d694d15ec6 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -1,14 +1,14 @@ import CryptoJS from 'crypto-js'; import CryptoJSPackageJson from 'crypto-js/package.json'; import { ALLOWED_GLOBALS } from './allowed-globals'; -import { isJSONPrimitive, JSONValue } from './json'; +import { isJSONPrimitive, JSONPrimitive } from './json'; import { WorkerEvents } from './shared-types'; interface WorkerData { request: { headers: Headers; }; - environmentVariables: Record; + environmentVariables: Record; } export type LogMessage = string | Error; diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index cf8668029a..5189a8b6b1 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { Kit } from '../kit'; -import { JSONValue } from './json'; +import { JSONPrimitive } from './json'; type _MessageEvent = MessageEvent; @@ -35,7 +35,7 @@ export namespace IFrameEvents { export interface Result { type: Event.result; runId: string; - environmentVariables: Record; + environmentVariables: Record; request: { headers: Kit.Headers.Encoded; }; @@ -80,7 +80,7 @@ export namespace IFrameEvents { type: Event.run; id: string; script: string; - environmentVariables: Record; + environmentVariables: Record; } export interface Abort { @@ -136,7 +136,7 @@ export namespace WorkerEvents { export interface Result { type: Event.result; - environmentVariables: Record; + environmentVariables: Record; request: { headers: Kit.Headers.Encoded; }; @@ -174,7 +174,7 @@ export namespace WorkerEvents { export interface Run { type: Event.run; script: string; - environmentVariables: Record; + environmentVariables: Record; } } diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 926795712e..b72a13c82c 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -58,8 +58,7 @@ import 'graphiql/style.css'; import '@graphiql/plugin-explorer/style.css'; import { PromptManager, PromptProvider } from '@/components/ui/prompt'; import { useRedirect } from '@/lib/access/common'; -import { Kit } from '@/lib/kit'; -import { JSONValue } from '@/lib/preflight-sandbox/json'; +import { JSONPrimitive } from '@/lib/preflight-sandbox/json'; import { captureException } from '@sentry/react'; const explorer = explorerPlugin(); @@ -260,7 +259,7 @@ function Save(props: { function substituteVariablesInHeaders( headers: Record, - environmentVariables: Record, + environmentVariables: Record, ) { return Object.fromEntries( Object.entries(headers).map(([key, value]) => { From a9dbd2a1268b1b349c419ebcf9450ec844dc8e6b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 15:06:23 -0500 Subject: [PATCH 29/45] move jsdoc --- .../app/src/lib/preflight-sandbox/lab-api-declaration.ts | 9 +++++++++ .../src/lib/preflight-sandbox/preflight-script-worker.ts | 6 ------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts index 7e2daf3ab7..05d7a5128e 100644 --- a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts +++ b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts @@ -8,6 +8,15 @@ // and use Prettier to format it, have syntax highlighting, etc. interface LabAPI { + /** + * Contains aspects of the request that you can manipulate before it is sent. + */ + request: { + /** + * The headers of the request. + */ + headers: Headers; + }; /** * [CryptoJS](https://cryptojs.gitbook.io/docs) library. */ diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts index d694d15ec6..ddbe837f5e 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -139,13 +139,7 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise { } }, }, - /** - * Contains aspects of the request that you can manipulate before it is sent. - */ request: { - /** - * The headers of the request. - */ headers: workerData.request.headers, }, /** From 51290a6dd18552e74846fa6a1fbd9204baefd3e9 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 15:32:11 -0500 Subject: [PATCH 30/45] no issue --- packages/web/app/src/pages/target-laboratory.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index b72a13c82c..86a3fb67b3 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -363,18 +363,6 @@ function LaboratoryPageContent(props: { // in preflight headers. // ...substituteVariablesInHeaders(opts?.headers ?? {}, preflightData.environmentVariables), - - // todo: test case covering this limitation - // todo: website documentation mentioning this limitation to our users. - // todo: jsdoc on `lab` mentioning this limitation to our users. - // todo: https://github.com/graphql/graphiql/pull/3854 - // We have submitted a PR to GraphiQL to fix the issue described below. - // Once shipped, remove our lossy code below. - // - // GraphiQLFetcher only support record-shaped headers which - // precludes complete usage of Headers data structure, namely where there are multiple values for one - // header. - // ...Object.fromEntries(preflightData.request.headers), }; From 9c13ef60376483fbecb0aab79859a3cb3219b300 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 15:42:51 -0500 Subject: [PATCH 31/45] explain merge strategy --- .../app/src/lib/preflight-sandbox/lab-api-declaration.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts index 05d7a5128e..555e16bbe6 100644 --- a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts +++ b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts @@ -13,7 +13,13 @@ interface LabAPI { */ request: { /** - * The headers of the request. + * Headers that will be added to the request. They are merged in as follows: + * + * 1. Do not support interpolation of environment variables. + * + * 2. Upon a collision with a base header, this header takes precedence. + * This means that if the base headers contain "foo: bar" and you add "foo: qux" + * here, the final headers become "foo: qux" (*not* "foo: bar, qux"). */ headers: Headers; }; From 14329fcd413f1e38e4ff4453757599ca46a3b7ef Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 15:43:49 -0500 Subject: [PATCH 32/45] tweak jsdoc --- .../web/app/src/lib/preflight-sandbox/lab-api-declaration.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts index 555e16bbe6..5850c8b9dd 100644 --- a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts +++ b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts @@ -13,9 +13,10 @@ interface LabAPI { */ request: { /** - * Headers that will be added to the request. They are merged in as follows: + * Headers that will be added to the request. They are merged + * using the following rules: * - * 1. Do not support interpolation of environment variables. + * 1. Do *not* interpolate environment variables. * * 2. Upon a collision with a base header, this header takes precedence. * This means that if the base headers contain "foo: bar" and you add "foo: qux" From 4e0d1df2f097b61bdaadee217a007424d48197ef Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 21 Jan 2025 15:44:24 -0500 Subject: [PATCH 33/45] tweak jsdoc --- .../web/app/src/lib/preflight-sandbox/lab-api-declaration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts index 5850c8b9dd..80ba87421a 100644 --- a/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts +++ b/packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts @@ -19,8 +19,8 @@ interface LabAPI { * 1. Do *not* interpolate environment variables. * * 2. Upon a collision with a base header, this header takes precedence. - * This means that if the base headers contain "foo: bar" and you add "foo: qux" - * here, the final headers become "foo: qux" (*not* "foo: bar, qux"). + * This means that if the base headers contain "foo: bar" and you've added + * "foo: qux" here, the final headers become "foo: qux" (*not* "foo: bar, qux"). */ headers: Headers; }; From 960714e9ba3811f0a632912955ef5f664edc9690 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 11:58:57 -0500 Subject: [PATCH 34/45] lint --- packages/web/app/src/lib/kit/index.ts | 1 + packages/web/app/src/lib/kit/never.ts | 2 +- packages/web/app/src/lib/kit/types/headers.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/web/app/src/lib/kit/index.ts b/packages/web/app/src/lib/kit/index.ts index b65599cad7..87c64a4bfd 100644 --- a/packages/web/app/src/lib/kit/index.ts +++ b/packages/web/app/src/lib/kit/index.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-self-import export * as Kit from './index'; export * from './never'; diff --git a/packages/web/app/src/lib/kit/never.ts b/packages/web/app/src/lib/kit/never.ts index b0bfb785cb..7eed399bf6 100644 --- a/packages/web/app/src/lib/kit/never.ts +++ b/packages/web/app/src/lib/kit/never.ts @@ -11,6 +11,6 @@ export const neverCase = (value: never): never => { * If it is reached, then that means there is a bug in our code. */ export const never: (contextMessage?: string) => never = contextMessage => { - contextMessage = contextMessage ?? '(no additional context provided)'; + contextMessage ??= '(no additional context provided)'; throw new Error(`Something that should be impossible happened: ${contextMessage}`); }; diff --git a/packages/web/app/src/lib/kit/types/headers.ts b/packages/web/app/src/lib/kit/types/headers.ts index 044edf4c87..177db6a4bf 100644 --- a/packages/web/app/src/lib/kit/types/headers.ts +++ b/packages/web/app/src/lib/kit/types/headers.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace Headers { export type Encoded = [name: string, value: string][]; } From f2e2ba84ad0d0e6599b25756bfbd24807c33e1fd Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 12:05:41 -0500 Subject: [PATCH 35/45] test todos --- cypress/e2e/laboratory-preflight-script.cy.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index 6529499c00..9210e1bf82 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -186,6 +186,12 @@ throw new TypeError('Test')`, }); describe('Execution', () => { + it('result.request.headers are added to the graphiql request base headers'); + + it('result.request.headers take precedence over graphiql request base headers'); + + it('result.request.headers do not receive placeholder substitution'); + it('header placeholders are substituted with environment variables', () => { cy.dataCy('toggle-preflight-script').click(); cy.get('[data-name="headers"]').click(); From e3692b8e468d7692511b2e3670684e3bae39f17a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 13:35:11 -0500 Subject: [PATCH 36/45] first test --- cypress/e2e/laboratory-preflight-script.cy.ts | 16 +++++++++++++++- packages/web/app/src/lib/kit/never.ts | 7 +++---- .../lib/preflight-sandbox/graphiql-plugin.tsx | 11 ++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index 9210e1bf82..cf627e4671 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -186,7 +186,21 @@ throw new TypeError('Test')`, }); describe('Execution', () => { - it('result.request.headers are added to the graphiql request base headers'); + it.only('result.request.headers are added to the graphiql request base headers', () => { + cy.dataCy('toggle-preflight-script').click(); + cy.dataCy('preflight-script-modal-button').click(); + setEditorScript(`lab.request.headers.append('x-foo', 'bar')`); + cy.dataCy('preflight-script-modal-submit').click(); + + cy.intercept({ + method: 'POST', + headers: { + 'x-foo': 'bar', + }, + }).as('post'); + cy.get('.graphiql-execute-button').click(); + cy.wait('@post'); + }); it('result.request.headers take precedence over graphiql request base headers'); diff --git a/packages/web/app/src/lib/kit/never.ts b/packages/web/app/src/lib/kit/never.ts index 7eed399bf6..7ae8562cae 100644 --- a/packages/web/app/src/lib/kit/never.ts +++ b/packages/web/app/src/lib/kit/never.ts @@ -3,14 +3,13 @@ * If it happens, then that means there is a bug in our code. */ export const neverCase = (value: never): never => { - never(`Unhandled case: ${String(value)}`); + never({ type: 'case', value }); }; /** * This code cannot be reached. * If it is reached, then that means there is a bug in our code. */ -export const never: (contextMessage?: string) => never = contextMessage => { - contextMessage ??= '(no additional context provided)'; - throw new Error(`Something that should be impossible happened: ${contextMessage}`); +export const never: (context?: object) => never = context => { + throw new Error(`Something that should be impossible happened`, { cause: context }); }; diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 4a1a4458e6..307ed7058a 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -341,7 +341,16 @@ export function usePreflightScript(args: { return; } - Kit.neverCase(ev.data); + // Window message events can be emitted from unknowable sources. + // For example when our e2e tests runs within Cypress GUI, we see a `MessageEvent` with `.data` of `{ vscodeScheduleAsyncWork: 3 }`. + // Since we cannot know if the event source is Preflight Script, we cannot perform an exhaustive check. + // + // Kit.neverCase(ev.data); + // + console.debug( + 'preflight sandbox graphiql plugin: An unknown window message event received. Ignoring.', + ev, + ); } window.addEventListener('message', eventHandler); From 285bf4d71305dd178cb70e63352f0a05ae720e59 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 14:37:28 -0500 Subject: [PATCH 37/45] found bug --- cypress/e2e/laboratory-preflight-script.cy.ts | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index cf627e4671..101182d0e3 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -1,5 +1,16 @@ import { dedent } from '../support/testkit'; +const selectors = { + buttonModal: 'preflight-script-modal-button', + buttonToggle: 'toggle-preflight-script', + graphiql: { + buttonExecute: '.graphiql-execute-button', + }, + modal: { + buttonSubmit: 'preflight-script-modal-submit', + }, +}; + beforeEach(() => { cy.clearLocalStorage().then(async () => { cy.task('seedTarget').then(({ slug, refreshToken }: any) => { @@ -186,23 +197,63 @@ throw new TypeError('Test')`, }); describe('Execution', () => { - it.only('result.request.headers are added to the graphiql request base headers', () => { - cy.dataCy('toggle-preflight-script').click(); - cy.dataCy('preflight-script-modal-button').click(); - setEditorScript(`lab.request.headers.append('x-foo', 'bar')`); - cy.dataCy('preflight-script-modal-submit').click(); - + it('result.request.headers are added to the graphiql request base headers', () => { + const headers = { + foo: { name: 'foo', value: 'bar' }, + }; + cy.dataCy(selectors.buttonToggle).click(); + cy.dataCy(selectors.buttonModal).click(); + setEditorScript(`lab.request.headers.append('${headers.foo.name}', '${headers.foo.value}')`); + cy.dataCy(selectors.modal.buttonSubmit).click(); cy.intercept({ method: 'POST', headers: { - 'x-foo': 'bar', + [headers.foo.name]: headers.foo.value, }, }).as('post'); - cy.get('.graphiql-execute-button').click(); + cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@post'); }); - it('result.request.headers take precedence over graphiql request base headers'); + it('result.request.headers take precedence over graphiql request base headers', () => { + // --- Pre Assert Integrity Check: make sure the header we think we're overriding is actually there. + const baseHeaders = { + accept: { + name: 'accept', + value: 'application/json, multipart/mixed', + }, + }; + cy.intercept({ + method: 'POST', + headers: { + [baseHeaders.accept.name]: baseHeaders.accept.value, + }, + }).as('integrityCheck'); + cy.get(selectors.graphiql.buttonExecute).click(); + cy.wait('@integrityCheck'); + // --- + + const preflightHeaders = { + accept: { + name: 'accept', + value: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', + }, + }; + cy.dataCy(selectors.buttonToggle).click(); + cy.dataCy(selectors.buttonModal).click(); + setEditorScript( + `lab.request.headers.append('${preflightHeaders.accept.name}', '${preflightHeaders.accept.value}')`, + ); + cy.dataCy(selectors.modal.buttonSubmit).click(); + cy.intercept({ + method: 'POST', + headers: { + [preflightHeaders.accept.name]: preflightHeaders.accept.value, + }, + }).as('post'); + cy.get(selectors.graphiql.buttonExecute).click(); + cy.wait('@post'); + }); it('result.request.headers do not receive placeholder substitution'); From d6bf9adddbe9191c30b9d36dcf0720ca89c852ce Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 14:48:41 -0500 Subject: [PATCH 38/45] lint --- packages/web/app/src/lib/kit/never.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/app/src/lib/kit/never.ts b/packages/web/app/src/lib/kit/never.ts index 7ae8562cae..1b7a40c724 100644 --- a/packages/web/app/src/lib/kit/never.ts +++ b/packages/web/app/src/lib/kit/never.ts @@ -11,5 +11,5 @@ export const neverCase = (value: never): never => { * If it is reached, then that means there is a bug in our code. */ export const never: (context?: object) => never = context => { - throw new Error(`Something that should be impossible happened`, { cause: context }); + throw new Error('Something that should be impossible happened', { cause: context }); }; From 51308fbf54754ae0caa3609a1fb1b182501f519e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 15:06:05 -0500 Subject: [PATCH 39/45] refactor --- cypress/e2e/laboratory-preflight-script.cy.ts | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index 101182d0e3..c23d8f35bc 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -199,17 +199,15 @@ throw new TypeError('Test')`, describe('Execution', () => { it('result.request.headers are added to the graphiql request base headers', () => { const headers = { - foo: { name: 'foo', value: 'bar' }, + foo: 'bar', }; cy.dataCy(selectors.buttonToggle).click(); cy.dataCy(selectors.buttonModal).click(); - setEditorScript(`lab.request.headers.append('${headers.foo.name}', '${headers.foo.value}')`); + setEditorScript(`lab.request.headers.append('foo', '${headers.foo}')`); cy.dataCy(selectors.modal.buttonSubmit).click(); cy.intercept({ method: 'POST', - headers: { - [headers.foo.name]: headers.foo.value, - }, + headers, }).as('post'); cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@post'); @@ -218,38 +216,26 @@ describe('Execution', () => { it('result.request.headers take precedence over graphiql request base headers', () => { // --- Pre Assert Integrity Check: make sure the header we think we're overriding is actually there. const baseHeaders = { - accept: { - name: 'accept', - value: 'application/json, multipart/mixed', - }, + accept: 'application/json, multipart/mixed', }; cy.intercept({ method: 'POST', - headers: { - [baseHeaders.accept.name]: baseHeaders.accept.value, - }, + headers: baseHeaders, }).as('integrityCheck'); cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@integrityCheck'); // --- const preflightHeaders = { - accept: { - name: 'accept', - value: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', - }, + accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', }; cy.dataCy(selectors.buttonToggle).click(); cy.dataCy(selectors.buttonModal).click(); - setEditorScript( - `lab.request.headers.append('${preflightHeaders.accept.name}', '${preflightHeaders.accept.value}')`, - ); + setEditorScript(`lab.request.headers.append('accept', '${preflightHeaders.accept}')`); cy.dataCy(selectors.modal.buttonSubmit).click(); cy.intercept({ method: 'POST', - headers: { - [preflightHeaders.accept.name]: preflightHeaders.accept.value, - }, + headers: preflightHeaders, }).as('post'); cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@post'); From 8c3606aec56ae134361db6430e970457a2376c7b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 15:36:22 -0500 Subject: [PATCH 40/45] finish tests --- cypress/e2e/laboratory-preflight-script.cy.ts | 85 +++++++++++++------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index c23d8f35bc..49df5236d2 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -1,13 +1,18 @@ import { dedent } from '../support/testkit'; const selectors = { - buttonModal: 'preflight-script-modal-button', - buttonToggle: 'toggle-preflight-script', + buttonModalCy: 'preflight-script-modal-button', + buttonToggleCy: 'toggle-preflight-script', + buttonHeaders: '[data-name="headers"]', + headersEditor: { + textArea: '.graphiql-editor-tool .graphiql-editor:last-child textarea', + }, graphiql: { buttonExecute: '.graphiql-execute-button', }, + modal: { - buttonSubmit: 'preflight-script-modal-submit', + buttonSubmitCy: 'preflight-script-modal-submit', }, }; @@ -198,30 +203,26 @@ throw new TypeError('Test')`, describe('Execution', () => { it('result.request.headers are added to the graphiql request base headers', () => { - const headers = { + const preflightHeaders = { foo: 'bar', }; - cy.dataCy(selectors.buttonToggle).click(); - cy.dataCy(selectors.buttonModal).click(); - setEditorScript(`lab.request.headers.append('foo', '${headers.foo}')`); - cy.dataCy(selectors.modal.buttonSubmit).click(); - cy.intercept({ - method: 'POST', - headers, - }).as('post'); + cy.dataCy(selectors.buttonToggleCy).click(); + cy.dataCy(selectors.buttonModalCy).click(); + setEditorScript(`lab.request.headers.append('foo', '${preflightHeaders.foo}')`); + cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.intercept({ headers: preflightHeaders }).as('request'); cy.get(selectors.graphiql.buttonExecute).click(); - cy.wait('@post'); + cy.wait('@request'); }); it('result.request.headers take precedence over graphiql request base headers', () => { - // --- Pre Assert Integrity Check: make sure the header we think we're overriding is actually there. + // --- Integrity Check: Ensure the header we think we're overriding is actually there to override. + // --- We achieve this by asserting a sent GraphiQL request includes the certain header and assume + // --- if its there once its there every time. const baseHeaders = { accept: 'application/json, multipart/mixed', }; - cy.intercept({ - method: 'POST', - headers: baseHeaders, - }).as('integrityCheck'); + cy.intercept({ headers: baseHeaders }).as('integrityCheck'); cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@integrityCheck'); // --- @@ -229,19 +230,49 @@ describe('Execution', () => { const preflightHeaders = { accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', }; - cy.dataCy(selectors.buttonToggle).click(); - cy.dataCy(selectors.buttonModal).click(); + cy.dataCy(selectors.buttonToggleCy).click(); + cy.dataCy(selectors.buttonModalCy).click(); setEditorScript(`lab.request.headers.append('accept', '${preflightHeaders.accept}')`); - cy.dataCy(selectors.modal.buttonSubmit).click(); - cy.intercept({ - method: 'POST', - headers: preflightHeaders, - }).as('post'); + cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.intercept({ headers: preflightHeaders }).as('request'); cy.get(selectors.graphiql.buttonExecute).click(); - cy.wait('@post'); + cy.wait('@request'); }); - it('result.request.headers do not receive placeholder substitution'); + it.only('result.request.headers are NOT substituted with environment variables', () => { + const barEnVarInterpolation = '{{bar}}'; + + const staticHeaders = { + foo_static: barEnVarInterpolation, + }; + cy.get(selectors.buttonHeaders).click(); + cy.get(selectors.headersEditor.textArea).type(JSON.stringify(staticHeaders), { + force: true, + parseSpecialCharSequences: false, + }); + + const environmentVariables = { + bar: 'BAR_VALUE', + }; + const preflightHeaders = { + foo_preflight: barEnVarInterpolation, + }; + cy.dataCy(selectors.buttonToggleCy).click(); + cy.dataCy(selectors.buttonModalCy).click(); + setEditorScript(` + lab.environment.set('bar', '${environmentVariables.bar}') + lab.request.headers.append('foo_preflight', '${preflightHeaders.foo_preflight}') + `); + cy.dataCy(selectors.modal.buttonSubmitCy).click(); + cy.intercept({ + headers: { + ...preflightHeaders, + foo_static: environmentVariables.bar, + }, + }).as('request'); + cy.get(selectors.graphiql.buttonExecute).click(); + cy.wait('@request'); + }); it('header placeholders are substituted with environment variables', () => { cy.dataCy('toggle-preflight-script').click(); From 8e949f67c93901298b62284351c795dd2983a6ef Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 15:38:48 -0500 Subject: [PATCH 41/45] refactor --- cypress/e2e/laboratory-preflight-script.cy.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index 49df5236d2..c3d83ac5ac 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -203,6 +203,7 @@ throw new TypeError('Test')`, describe('Execution', () => { it('result.request.headers are added to the graphiql request base headers', () => { + // Setup Preflight Script const preflightHeaders = { foo: 'bar', }; @@ -210,23 +211,23 @@ describe('Execution', () => { cy.dataCy(selectors.buttonModalCy).click(); setEditorScript(`lab.request.headers.append('foo', '${preflightHeaders.foo}')`); cy.dataCy(selectors.modal.buttonSubmitCy).click(); + // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@request'); }); it('result.request.headers take precedence over graphiql request base headers', () => { - // --- Integrity Check: Ensure the header we think we're overriding is actually there to override. - // --- We achieve this by asserting a sent GraphiQL request includes the certain header and assume - // --- if its there once its there every time. + // Integrity Check: Ensure the header we think we're overriding is actually there to override. + // We achieve this by asserting a sent GraphiQL request includes the certain header and assume + // if its there once its there every time. const baseHeaders = { accept: 'application/json, multipart/mixed', }; cy.intercept({ headers: baseHeaders }).as('integrityCheck'); cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@integrityCheck'); - // --- - + // Setup Preflight Script const preflightHeaders = { accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', }; @@ -234,6 +235,7 @@ describe('Execution', () => { cy.dataCy(selectors.buttonModalCy).click(); setEditorScript(`lab.request.headers.append('accept', '${preflightHeaders.accept}')`); cy.dataCy(selectors.modal.buttonSubmitCy).click(); + // Run GraphiQL cy.intercept({ headers: preflightHeaders }).as('request'); cy.get(selectors.graphiql.buttonExecute).click(); cy.wait('@request'); @@ -241,7 +243,7 @@ describe('Execution', () => { it.only('result.request.headers are NOT substituted with environment variables', () => { const barEnVarInterpolation = '{{bar}}'; - + // Setup Static Headers const staticHeaders = { foo_static: barEnVarInterpolation, }; @@ -250,7 +252,7 @@ describe('Execution', () => { force: true, parseSpecialCharSequences: false, }); - + // Setup Preflight Script const environmentVariables = { bar: 'BAR_VALUE', }; @@ -264,6 +266,7 @@ describe('Execution', () => { lab.request.headers.append('foo_preflight', '${preflightHeaders.foo_preflight}') `); cy.dataCy(selectors.modal.buttonSubmitCy).click(); + // Run GraphiQL cy.intercept({ headers: { ...preflightHeaders, From 07b24e8ee7c5ea8d16fd4d050f96dfc9f23ff60a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 17:49:13 -0500 Subject: [PATCH 42/45] changelog --- .changeset/empty-rockets-smell.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changeset/empty-rockets-smell.md diff --git a/.changeset/empty-rockets-smell.md b/.changeset/empty-rockets-smell.md new file mode 100644 index 0000000000..803191d8b5 --- /dev/null +++ b/.changeset/empty-rockets-smell.md @@ -0,0 +1,16 @@ +--- +'hive': minor +--- + +You can now set HTTP headers in your Console Laboratory Preflight Script. Every time you run a request from Laboratory, your preflight headers, if any, will be merged into the request before it is sent. + +You achieve this by interacting with the [`Headers`](https://developer.mozilla.org/docs/web/api/headers) instance newly available at `lab.request.headers`. For example, this script would would add a `foo` header with the value `bar` to every Laboratory request. + +```ts +lab.request.headers.set('foo', 'bar') +``` + +A few notes about how headers are merged: + +1. Unlike static headers, preflight headers do not receive environment variable substitutions on their values. +2. Preflight headers take precedence, overwriting any same-named headers already in the Laboratory request. From 9ccb39f9e5fe73df65b1e61730fae6324355a544 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 22 Jan 2025 17:58:16 -0500 Subject: [PATCH 43/45] docs --- .changeset/empty-rockets-smell.md | 4 +++- .../laboratory/preflight-scripts.mdx | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.changeset/empty-rockets-smell.md b/.changeset/empty-rockets-smell.md index 803191d8b5..d06941a545 100644 --- a/.changeset/empty-rockets-smell.md +++ b/.changeset/empty-rockets-smell.md @@ -2,7 +2,7 @@ 'hive': minor --- -You can now set HTTP headers in your Console Laboratory Preflight Script. Every time you run a request from Laboratory, your preflight headers, if any, will be merged into the request before it is sent. +You can now set HTTP headers in your [Laboratory Preflight Script](https://the-guild.dev/graphql/hive/docs/dashboard/laboratory/preflight-scripts). Every time you run a request from Laboratory, your preflight headers, if any, will be merged into the request before it is sent. You achieve this by interacting with the [`Headers`](https://developer.mozilla.org/docs/web/api/headers) instance newly available at `lab.request.headers`. For example, this script would would add a `foo` header with the value `bar` to every Laboratory request. @@ -14,3 +14,5 @@ A few notes about how headers are merged: 1. Unlike static headers, preflight headers do not receive environment variable substitutions on their values. 2. Preflight headers take precedence, overwriting any same-named headers already in the Laboratory request. + +Documentation for this new feature is available at https://the-guild.dev/graphql/hive/docs/dashboard/laboratory/preflight-scripts#http-headers. diff --git a/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-scripts.mdx b/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-scripts.mdx index 8768c12899..c6cc5a8a10 100644 --- a/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-scripts.mdx +++ b/packages/web/docs/src/pages/docs/dashboard/laboratory/preflight-scripts.mdx @@ -98,6 +98,28 @@ organization. ![](./editing.png) +## HTTP Headers + +You can set HTTP headers in your script. Every time you run a request from Laboratory, your +preflight headers, if any, will be merged into the request before it is sent. + +You achieve this by interacting with the +[`Headers`](https://developer.mozilla.org/docs/web/api/headers) instance available at +`lab.request.headers`. For example, this script would would add a `foo` header with the value `bar` +to every Laboratory request. + +```ts +lab.request.headers.set('foo', 'bar') +``` + +Preflight headers are merged with the following rules: + +1. Unlike static headers, preflight headers do not receive + [environment variable substitutions](#using-environment-variables-in-http-headers) on their + values. +2. Preflight headers take precedence, overwriting any same-named headers already in the Laboratory + request. + ## Using Environment Variables in HTTP Headers To use environment variables in the HTTP headers of your GraphQL requests, wrap the keys in double From 5061b30baf520223fe3ee0988053991d15a942b5 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 23 Jan 2025 08:58:44 -0500 Subject: [PATCH 44/45] no only --- cypress/e2e/laboratory-preflight-script.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index c3d83ac5ac..c935988df8 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -241,7 +241,7 @@ describe('Execution', () => { cy.wait('@request'); }); - it.only('result.request.headers are NOT substituted with environment variables', () => { + it('result.request.headers are NOT substituted with environment variables', () => { const barEnVarInterpolation = '{{bar}}'; // Setup Static Headers const staticHeaders = { From 6f422232eca77a3a4f035882d27761643315418c Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Thu, 23 Jan 2025 08:59:53 -0500 Subject: [PATCH 45/45] todo done --- packages/web/app/src/pages/target-laboratory.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 43d24ad5d8..c563202898 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -357,7 +357,6 @@ function LaboratoryPageContent(props: { } const headers = { - // todo: test case covering point below // We want to prevent users from interpolating environment variables into // their preflight script headers. So, apply substitution BEFORE merging // in preflight headers.