Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(laboratory/preflight-script): set request headers #6378

Merged
merged 49 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
cb800d3
feat(laboratory/preflight-sandbox): allow appending headers
jasonkuhrt Jan 16, 2025
fac4434
typo fix
jasonkuhrt Jan 17, 2025
4084026
remove if
jasonkuhrt Jan 20, 2025
0c6d465
expose headers
jasonkuhrt Jan 20, 2025
a21ea80
finish refactor: connected event type defs
jasonkuhrt Jan 20, 2025
cd12274
no substitute headers preflight script
jasonkuhrt Jan 20, 2025
fc2f8c9
embrace headers limitation for now
jasonkuhrt Jan 20, 2025
b5f21b0
no type-encoding of passthrough
jasonkuhrt Jan 20, 2025
e24df4d
fix
jasonkuhrt Jan 20, 2025
fbe7eee
mention todo
jasonkuhrt Jan 20, 2025
998c566
make noop cases debuggable
jasonkuhrt Jan 20, 2025
28b8bbb
kit json decode safe
jasonkuhrt Jan 20, 2025
557bc4e
wip
jasonkuhrt Jan 21, 2025
6db0e1c
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 21, 2025
52222f0
wip
jasonkuhrt Jan 21, 2025
1212fc8
Revert "wip"
jasonkuhrt Jan 21, 2025
c890264
Revert "wip"
jasonkuhrt Jan 21, 2025
04a96fd
work
jasonkuhrt Jan 21, 2025
0b79e9f
simplify
jasonkuhrt Jan 21, 2025
09f15c6
refactor
jasonkuhrt Jan 21, 2025
e3c253b
use const
jasonkuhrt Jan 21, 2025
15b7666
use const
jasonkuhrt Jan 21, 2025
2622f99
reduce diff
jasonkuhrt Jan 21, 2025
525cb4e
refactor
jasonkuhrt Jan 21, 2025
3541df7
untodo
jasonkuhrt Jan 21, 2025
dfce1c4
explicit string
jasonkuhrt Jan 21, 2025
c8a62a4
remove unused code
jasonkuhrt Jan 21, 2025
231f7ad
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 21, 2025
8f36d86
use existing json module
jasonkuhrt Jan 21, 2025
f806027
fix: json primitive value
jasonkuhrt Jan 21, 2025
a9dbd2a
move jsdoc
jasonkuhrt Jan 21, 2025
51290a6
no issue
jasonkuhrt Jan 21, 2025
9c13ef6
explain merge strategy
jasonkuhrt Jan 21, 2025
14329fc
tweak jsdoc
jasonkuhrt Jan 21, 2025
4e0d1df
tweak jsdoc
jasonkuhrt Jan 21, 2025
c963e46
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 22, 2025
960714e
lint
jasonkuhrt Jan 22, 2025
f2e2ba8
test todos
jasonkuhrt Jan 22, 2025
e3692b8
first test
jasonkuhrt Jan 22, 2025
285bf4d
found bug
jasonkuhrt Jan 22, 2025
d6bf9ad
lint
jasonkuhrt Jan 22, 2025
51308fb
refactor
jasonkuhrt Jan 22, 2025
8c3606a
finish tests
jasonkuhrt Jan 22, 2025
8e949f6
refactor
jasonkuhrt Jan 22, 2025
07b24e8
changelog
jasonkuhrt Jan 22, 2025
9ccb39f
docs
jasonkuhrt Jan 22, 2025
5061b30
no only
jasonkuhrt Jan 23, 2025
6f42223
todo done
jasonkuhrt Jan 23, 2025
8c75db9
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions cypress/e2e/laboratory-preflight-script.cy.ts
Original file line number Diff line number Diff line change
@@ -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',
},
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you like this pattern I can apply it consistently elsewhere now or later.


beforeEach(() => {
cy.clearLocalStorage().then(async () => {
cy.task('seedTarget').then(({ slug, refreshToken }: any) => {
Expand Down Expand Up @@ -186,6 +197,66 @@ 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' },
};
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: {
[headers.foo.name]: headers.foo.value,
},
}).as('post');
cy.get(selectors.graphiql.buttonExecute).click();
cy.wait('@post');
});

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');

it('header placeholders are substituted with environment variables', () => {
cy.dataCy('toggle-preflight-script').click();
cy.get('[data-name="headers"]').click();
Expand Down
10 changes: 10 additions & 0 deletions packages/web/app/src/lib/kit/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const tryOr = <$PrimaryResult, $FallbackResult>(
fn: () => $PrimaryResult,
fallback: () => $FallbackResult,
): $PrimaryResult | $FallbackResult => {
try {
return fn();
} catch {
return fallback();
}
};
6 changes: 6 additions & 0 deletions packages/web/app/src/lib/kit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// eslint-disable-next-line import/no-self-import
export * as Kit from './index';

export * from './never';
export * from './types/headers';
export * from './helpers';
15 changes: 15 additions & 0 deletions packages/web/app/src/lib/kit/never.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* This case is impossible.
* If it happens, then that means there is a bug in our code.
*/
export const neverCase = (value: never): never => {
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: (context?: object) => never = context => {
throw new Error(`Something that should be impossible happened`, { cause: context });

Check failure on line 14 in packages/web/app/src/lib/kit/never.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

Strings must use singlequote
};
4 changes: 4 additions & 0 deletions packages/web/app/src/lib/kit/types/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Headers {
export type Encoded = [name: string, value: string][];
}
84 changes: 63 additions & 21 deletions packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@ import { Editor as MonacoEditor, OnMount, type Monaco } from '@monaco-editor/rea
import { Cross2Icon, InfoCircledIcon, Pencil1Icon, TriangleRightIcon } from '@radix-ui/react-icons';
import { captureException } from '@sentry/react';
import { useParams } from '@tanstack/react-router';
import { Kit } from '../kit';
import { cn } from '../utils';
import labApiDefinitionRaw from './lab-api-declaration?raw';
import { IFrameEvents, LogMessage } from './shared-types';

export type PreflightScriptResultData = Omit<
IFrameEvents.Outgoing.EventData.Result,
'type' | 'runId'
>;

export const preflightScriptPlugin: GraphiQLPlugin = {
icon: () => (
<svg
Expand Down Expand Up @@ -135,14 +141,6 @@ const PreflightScript_TargetFragment = graphql(`

export type LogRecord = LogMessage | { type: 'separator' };

function safeParseJSON(str: string): Record<string, unknown> | null {
try {
return JSON.parse(str);
} catch {
return null;
}
}

export const enum PreflightWorkerState {
running,
ready,
Expand Down Expand Up @@ -173,9 +171,24 @@ export function usePreflightScript(args: {

const currentRun = useRef<null | Function>(null);

async function execute(script = target?.preflightScript?.sourceCode ?? '', isPreview = false) {
async function execute(
script = target?.preflightScript?.sourceCode ?? '',
isPreview = false,
): Promise<PreflightScriptResultData> {
const resultEnvironmentVariablesDecoded: PreflightScriptResultData['environmentVariables'] =
Kit.tryOr(
() => JSON.parse(latestEnvironmentVariablesRef.current),
() => ({}),
);
const result: PreflightScriptResultData = {
request: {
headers: [],
},
environmentVariables: resultEnvironmentVariablesDecoded,
};

if (isPreview === false && !isPreflightScriptEnabled) {
return safeParseJSON(latestEnvironmentVariablesRef.current);
return result;
}

const id = crypto.randomUUID();
Expand All @@ -201,7 +214,8 @@ export function usePreflightScript(args: {
type: IFrameEvents.Incoming.Event.run,
id,
script,
environmentVariables: (environmentVariables && safeParseJSON(environmentVariables)) || {},
// Preflight Script has read/write relationship with environment variables.
environmentVariables: result.environmentVariables,
} satisfies IFrameEvents.Incoming.EventData,
'*',
);
Expand Down Expand Up @@ -257,16 +271,23 @@ export function usePreflightScript(args: {
}

if (ev.data.type === IFrameEvents.Outgoing.Event.result) {
const mergedEnvironmentVariables = JSON.stringify(
{
...safeParseJSON(latestEnvironmentVariablesRef.current),
...ev.data.environmentVariables,
},
const mergedEnvironmentVariables = {
...result.environmentVariables,
...ev.data.environmentVariables,
};
result.environmentVariables = mergedEnvironmentVariables;
result.request.headers = ev.data.request.headers;

// Cause the new state of environment variables to be
// written back to local storage.
const mergedEnvironmentVariablesEncoded = JSON.stringify(
result.environmentVariables,
null,
2,
);
setEnvironmentVariables(mergedEnvironmentVariables);
latestEnvironmentVariablesRef.current = mergedEnvironmentVariables;
setEnvironmentVariables(mergedEnvironmentVariablesEncoded);
latestEnvironmentVariablesRef.current = mergedEnvironmentVariablesEncoded;

setLogs(logs => [
...logs,
{
Expand Down Expand Up @@ -301,7 +322,6 @@ export function usePreflightScript(args: {
]);
setFinished();
closedOpenedPrompts();

return;
}

Expand All @@ -310,6 +330,27 @@ export function usePreflightScript(args: {
setLogs(logs => [...logs, log]);
return;
}

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) {
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved
console.debug('preflight sandbox graphiql plugin: noop iframe event:', ev.data);
return;
}

// 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);
Expand All @@ -328,7 +369,8 @@ export function usePreflightScript(args: {
window.removeEventListener('message', eventHandler);

setState(PreflightWorkerState.ready);
return safeParseJSON(latestEnvironmentVariablesRef.current);

return result;
} catch (err) {
if (err instanceof Error) {
setLogs(prev => [
Expand All @@ -346,7 +388,7 @@ export function usePreflightScript(args: {
},
]);
setState(PreflightWorkerState.ready);
return safeParseJSON(latestEnvironmentVariablesRef.current);
return result;
}
throw err;
}
Expand Down
16 changes: 16 additions & 0 deletions packages/web/app/src/lib/preflight-sandbox/lab-api-declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@
// 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: {
/**
* Headers that will be added to the request. They are merged
* using the following rules:
*
* 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've added
* "foo: qux" here, the final headers become "foo: qux" (*not* "foo: bar, qux").
*/
headers: Headers;
};
/**
* [CryptoJS](https://cryptojs.gitbook.io/docs) library.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import CryptoJS from 'crypto-js';
import CryptoJSPackageJson from 'crypto-js/package.json';
import { ALLOWED_GLOBALS } from './allowed-globals';
import { isJSONPrimitive } from './json';
import { isJSONPrimitive, JSONPrimitive } from './json';
import { LogMessage, WorkerEvents } from './shared-types';

interface WorkerData {
request: {
headers: Headers;
};
environmentVariables: Record<string, JSONPrimitive>;
}

/**
* Unique id for each prompt request.
* Incremented each time a prompt is requested.
Expand Down Expand Up @@ -45,11 +52,16 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise<void> {
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 = {
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 },
};

// 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);
Expand Down Expand Up @@ -123,17 +135,20 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise<void> {
},
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;
}
},
},
request: {
headers: workerData.request.headers,
},
/**
* Mimics the `prompt` function in the browser, by sending a message to the main thread
* and waiting for a response.
Expand Down Expand Up @@ -179,9 +194,13 @@ ${script}})()`;
});
return;
}

sendMessage({
type: WorkerEvents.Outgoing.Event.result,
environmentVariables: workingEnvironmentVariables,
environmentVariables: workerData.environmentVariables,
request: {
headers: Array.from(workerData.request.headers.entries()),
},
});
}

Expand Down
Loading
Loading