mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
feat: preflight scripts for laboratory (#5564)
Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com> Co-authored-by: Saihajpreet Singh <saihajpreet.singh@gmail.com> Co-authored-by: Laurin Quast <laurinquast@googlemail.com> Co-authored-by: Dotan Simha <dotansimha@gmail.com>
This commit is contained in:
parent
38c14e21d8
commit
e0eb3bdb28
60 changed files with 2882 additions and 345 deletions
8
.changeset/dull-seas-remember.md
Normal file
8
.changeset/dull-seas-remember.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
'hive': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add preflight scripts for laboratory.
|
||||||
|
|
||||||
|
It is now possible to add a preflight script within the laboratory that executes before sending a GraphQL request.
|
||||||
|
[Learn more.](https://the-guild.dev/graphql/hive/product-updates/2024-12-27-preflight-script)
|
||||||
6
.github/workflows/tests-e2e.yaml
vendored
6
.github/workflows/tests-e2e.yaml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
- name: setup environment
|
- name: setup environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
codegen: false
|
codegen: true
|
||||||
actor: test-e2e
|
actor: test-e2e
|
||||||
cacheTurbo: false
|
cacheTurbo: false
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ jobs:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
run: |
|
run: |
|
||||||
docker compose \
|
docker compose \
|
||||||
--env-file docker/.end2end.env \
|
--env-file integration-tests/.env \
|
||||||
-f docker/docker-compose.community.yml \
|
-f docker/docker-compose.community.yml \
|
||||||
-f docker/docker-compose.end2end.yml \
|
-f docker/docker-compose.end2end.yml \
|
||||||
up -d --wait
|
up -d --wait
|
||||||
|
|
@ -65,7 +65,7 @@ jobs:
|
||||||
docker --version
|
docker --version
|
||||||
docker ps --format json | jq .
|
docker ps --format json | jq .
|
||||||
docker compose \
|
docker compose \
|
||||||
--env-file docker/.end2end.env \
|
--env-file integration-tests/.env \
|
||||||
-f docker/docker-compose.community.yml \
|
-f docker/docker-compose.community.yml \
|
||||||
-f docker/docker-compose.end2end.yml \
|
-f docker/docker-compose.end2end.yml \
|
||||||
logs
|
logs
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,38 @@
|
||||||
|
import * as fs from 'node:fs';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies -- cypress SHOULD be a dev dependency
|
// eslint-disable-next-line import/no-extraneous-dependencies -- cypress SHOULD be a dev dependency
|
||||||
import fs from 'node:fs';
|
|
||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
|
import { initSeed } from './integration-tests/testkit/seed';
|
||||||
|
|
||||||
|
if (!process.env.RUN_AGAINST_LOCAL_SERVICES) {
|
||||||
|
const dotenv = await import('dotenv');
|
||||||
|
dotenv.config({ path: import.meta.dirname + '/integration-tests/.env' });
|
||||||
|
}
|
||||||
|
|
||||||
const isCI = Boolean(process.env.CI);
|
const isCI = Boolean(process.env.CI);
|
||||||
|
|
||||||
|
export const seed = initSeed();
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
video: isCI,
|
video: isCI,
|
||||||
screenshotOnRunFailure: isCI,
|
screenshotOnRunFailure: isCI,
|
||||||
defaultCommandTimeout: 15_000, // sometimes the app takes longer to load, especially in the CI
|
defaultCommandTimeout: 15_000, // sometimes the app takes longer to load, especially in the CI
|
||||||
retries: 2,
|
retries: 2,
|
||||||
env: {
|
|
||||||
POSTGRES_URL: 'postgresql://postgres:postgres@localhost:5432/registry',
|
|
||||||
},
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on) {
|
setupNodeEvents(on) {
|
||||||
|
on('task', {
|
||||||
|
async seedTarget() {
|
||||||
|
const owner = await seed.createOwner();
|
||||||
|
const org = await owner.createOrg();
|
||||||
|
const project = await org.createProject();
|
||||||
|
const slug = `${org.organization.slug}/${project.project.slug}/${project.target.slug}`;
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
refreshToken: owner.ownerRefreshToken,
|
||||||
|
email: owner.ownerEmail,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
on('after:spec', (_, results) => {
|
on('after:spec', (_, results) => {
|
||||||
if (results && results.video) {
|
if (results && results.video) {
|
||||||
// Do we have failures for any retry attempts?
|
// Do we have failures for any retry attempts?
|
||||||
|
|
|
||||||
196
cypress/e2e/preflight-script.cy.ts
Normal file
196
cypress/e2e/preflight-script.cy.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.clearLocalStorage().then(async () => {
|
||||||
|
cy.task('seedTarget').then(({ slug, refreshToken }: any) => {
|
||||||
|
cy.setCookie('sRefreshToken', refreshToken);
|
||||||
|
|
||||||
|
cy.visit(`/${slug}/laboratory`);
|
||||||
|
cy.get('[aria-label*="Preflight Script"]').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Helper function for setting the text within a monaco editor as typing manually results in flaky tests */
|
||||||
|
function setMonacoEditorContents(editorCyName: string, text: string) {
|
||||||
|
// wait for textarea appearing which indicates monaco is loaded
|
||||||
|
cy.dataCy(editorCyName).find('textarea');
|
||||||
|
cy.window().then(win => {
|
||||||
|
// First, check if monaco is available on the main window
|
||||||
|
const editor = (win as any).monaco.editor
|
||||||
|
.getEditors()
|
||||||
|
.find(e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorCyName);
|
||||||
|
|
||||||
|
// If Monaco instance is found
|
||||||
|
if (editor) {
|
||||||
|
editor.setValue(text);
|
||||||
|
} else {
|
||||||
|
throw new Error('Monaco editor not found on the window or frames[0]');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorScript(script: string) {
|
||||||
|
setMonacoEditorContents('preflight-script-editor', script);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Preflight Script', () => {
|
||||||
|
it('mini script editor is read only', () => {
|
||||||
|
cy.dataCy('toggle-preflight-script').click();
|
||||||
|
// Wait loading disappears
|
||||||
|
cy.dataCy('preflight-script-editor-mini').should('not.contain', 'Loading');
|
||||||
|
// Click
|
||||||
|
cy.dataCy('preflight-script-editor-mini').click();
|
||||||
|
// And type
|
||||||
|
cy.dataCy('preflight-script-editor-mini').within(() => {
|
||||||
|
cy.get('textarea').type('🐝', { force: true });
|
||||||
|
});
|
||||||
|
cy.dataCy('preflight-script-editor-mini').should(
|
||||||
|
'have.text',
|
||||||
|
'Cannot edit in read-only editor',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Preflight Script Modal', () => {
|
||||||
|
const script = 'console.log("Hello_world")';
|
||||||
|
const env = '{"foo":123}';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.dataCy('preflight-script-modal-button').click();
|
||||||
|
setMonacoEditorContents('env-editor', env);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('save script and environment variables when submitting', () => {
|
||||||
|
setEditorScript(script);
|
||||||
|
cy.dataCy('preflight-script-modal-submit').click();
|
||||||
|
cy.dataCy('env-editor-mini').should('have.text', env);
|
||||||
|
cy.dataCy('toggle-preflight-script').click();
|
||||||
|
cy.dataCy('preflight-script-editor-mini').should('have.text', script);
|
||||||
|
cy.reload();
|
||||||
|
cy.get('[aria-label*="Preflight Script"]').click();
|
||||||
|
cy.dataCy('env-editor-mini').should('have.text', env);
|
||||||
|
cy.dataCy('preflight-script-editor-mini').should('have.text', script);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs show console/error information', () => {
|
||||||
|
setEditorScript(script);
|
||||||
|
cy.dataCy('run-preflight-script').click();
|
||||||
|
cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)');
|
||||||
|
|
||||||
|
setEditorScript(
|
||||||
|
`console.info(1)
|
||||||
|
console.warn(true)
|
||||||
|
console.error('Fatal')
|
||||||
|
throw new TypeError('Test')`,
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.dataCy('run-preflight-script').click();
|
||||||
|
// First log previous log message
|
||||||
|
cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)');
|
||||||
|
// After the new logs
|
||||||
|
cy.dataCy('console-output').should(
|
||||||
|
'contain',
|
||||||
|
[
|
||||||
|
'Info: 1 (Line: 1, Column: 1)',
|
||||||
|
'Warn: true (Line: 2, Column: 1)',
|
||||||
|
'Error: Fatal (Line: 3, Column: 1)',
|
||||||
|
'TypeError: Test (Line: 4, Column: 7)',
|
||||||
|
].join(''),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('script execution updates environment variables', () => {
|
||||||
|
setEditorScript(`lab.environment.set('my-test', "TROLOLOL")`);
|
||||||
|
|
||||||
|
cy.dataCy('run-preflight-script').click();
|
||||||
|
cy.dataCy('env-editor').should(
|
||||||
|
'include.text',
|
||||||
|
// replace space with
|
||||||
|
'{ "foo": 123, "my-test": "TROLOLOL"}'.replaceAll(' ', '\xa0'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`crypto-js` can be used for generating hashes', () => {
|
||||||
|
setEditorScript('console.log(lab.CryptoJS.SHA256("🐝"))');
|
||||||
|
cy.dataCy('run-preflight-script').click();
|
||||||
|
cy.dataCy('console-output').should('contain', 'Info: Using crypto-js version:');
|
||||||
|
cy.dataCy('console-output').should(
|
||||||
|
'contain',
|
||||||
|
'Log: d5b51e79e4be0c4f4d6b9a14e16ca864de96afe68459e60a794e80393a4809e8',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scripts can not use `eval`', () => {
|
||||||
|
setEditorScript('eval()');
|
||||||
|
cy.dataCy('preflight-script-modal-submit').click();
|
||||||
|
cy.get('body').contains('Usage of dangerous statement like eval() or Function("").');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalid code is rejected and can not be saved', () => {
|
||||||
|
setEditorScript('🐝');
|
||||||
|
cy.dataCy('preflight-script-modal-submit').click();
|
||||||
|
cy.get('body').contains("[1:1]: Illegal character '}");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Execution', () => {
|
||||||
|
it('header placeholders are substituted with environment variables', () => {
|
||||||
|
cy.dataCy('toggle-preflight-script').click();
|
||||||
|
cy.get('[data-name="headers"]').click();
|
||||||
|
cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type(
|
||||||
|
'{ "__test": "{{foo}} bar {{nonExist}}" }',
|
||||||
|
{
|
||||||
|
force: true,
|
||||||
|
parseSpecialCharSequences: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cy.dataCy('env-editor-mini').within(() => {
|
||||||
|
cy.get('textarea').type('{"foo":"injected"}', {
|
||||||
|
force: true,
|
||||||
|
parseSpecialCharSequences: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.intercept('/api/lab/foo/my-new-project/development', req => {
|
||||||
|
expect(req.headers.__test).to.equal('injected bar {{nonExist}}');
|
||||||
|
});
|
||||||
|
cy.get('body').type('{ctrl}{enter}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executed script updates update env editor and substitute headers', () => {
|
||||||
|
cy.dataCy('toggle-preflight-script').click();
|
||||||
|
cy.get('[data-name="headers"]').click();
|
||||||
|
cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type(
|
||||||
|
'{ "__test": "{{foo}}" }',
|
||||||
|
{
|
||||||
|
force: true,
|
||||||
|
parseSpecialCharSequences: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cy.dataCy('preflight-script-modal-button').click();
|
||||||
|
setMonacoEditorContents('preflight-script-editor', `lab.environment.set('foo', 92)`);
|
||||||
|
cy.dataCy('preflight-script-modal-submit').click();
|
||||||
|
cy.intercept('/api/lab/foo/my-new-project/development', req => {
|
||||||
|
expect(req.headers.__test).to.equal('92');
|
||||||
|
});
|
||||||
|
cy.get('.graphiql-execute-button').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disabled script is not executed', () => {
|
||||||
|
cy.get('[data-name="headers"]').click();
|
||||||
|
cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type(
|
||||||
|
'{ "__test": "{{foo}}" }',
|
||||||
|
{
|
||||||
|
force: true,
|
||||||
|
parseSpecialCharSequences: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cy.dataCy('preflight-script-modal-button').click();
|
||||||
|
setMonacoEditorContents('preflight-script-editor', `lab.environment.set('foo', 92)`);
|
||||||
|
setMonacoEditorContents('env-editor', `{"foo":10}`);
|
||||||
|
|
||||||
|
cy.dataCy('preflight-script-modal-submit').click();
|
||||||
|
cy.intercept('/api/lab/foo/my-new-project/development', req => {
|
||||||
|
expect(req.headers.__test).to.equal('10');
|
||||||
|
});
|
||||||
|
cy.get('.graphiql-execute-button').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -25,7 +25,7 @@ cd ..
|
||||||
docker buildx bake -f docker/docker.hcl build --load
|
docker buildx bake -f docker/docker.hcl build --load
|
||||||
|
|
||||||
echo "⬆️ Running all local containers..."
|
echo "⬆️ Running all local containers..."
|
||||||
docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env --env-file ./docker/.end2end.env up -d --wait
|
docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env up -d --wait
|
||||||
|
|
||||||
echo "✅ E2E tests environment is ready. To run tests now, use:"
|
echo "✅ E2E tests environment is ready. To run tests now, use:"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es2021",
|
||||||
"lib": ["es5", "dom"],
|
"lib": ["es2021", "dom"],
|
||||||
"types": ["node", "cypress"]
|
"types": ["node", "cypress"]
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"]
|
"include": ["**/*.ts", "../integration-tests/testkit/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -335,12 +335,10 @@ deployCloudFlareSecurityTransform({
|
||||||
// Staging
|
// Staging
|
||||||
'staging.graphql-hive.com',
|
'staging.graphql-hive.com',
|
||||||
'app.staging.graphql-hive.com',
|
'app.staging.graphql-hive.com',
|
||||||
'lab-worker.staging.graphql-hive.com',
|
|
||||||
'cdn.staging.graphql-hive.com',
|
'cdn.staging.graphql-hive.com',
|
||||||
// Dev
|
// Dev
|
||||||
'dev.graphql-hive.com',
|
'dev.graphql-hive.com',
|
||||||
'app.dev.graphql-hive.com',
|
'app.dev.graphql-hive.com',
|
||||||
'lab-worker.dev.graphql-hive.com',
|
|
||||||
'cdn.dev.graphql-hive.com',
|
'cdn.dev.graphql-hive.com',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -353,4 +351,4 @@ export const schemaApiServiceId = schema.service.id;
|
||||||
export const webhooksApiServiceId = webhooks.service.id;
|
export const webhooksApiServiceId = webhooks.service.id;
|
||||||
|
|
||||||
export const appId = app.deployment.id;
|
export const appId = app.deployment.id;
|
||||||
export const publicIp = proxy!.status.loadBalancer.ingress[0].ip;
|
export const publicIp = proxy.get()!.status.loadBalancer.ingress[0].ip;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
import * as pulumi from '@pulumi/pulumi';
|
import * as pulumi from '@pulumi/pulumi';
|
||||||
import { serviceLocalEndpoint } from '../utils/local-endpoint';
|
import { serviceLocalEndpoint } from '../utils/local-endpoint';
|
||||||
import { ServiceDeployment } from '../utils/service-deployment';
|
import { ServiceDeployment } from '../utils/service-deployment';
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ export function deployCloudFlareSecurityTransform(options: {
|
||||||
)} } and not http.host in { ${toExpressionList(options.ignoredHosts)} }`;
|
)} } and not http.host in { ${toExpressionList(options.ignoredHosts)} }`;
|
||||||
|
|
||||||
// TODO: When Preflight PR is merged, we'll need to change this to build this host in a better way.
|
// TODO: When Preflight PR is merged, we'll need to change this to build this host in a better way.
|
||||||
const labHost = `lab-worker.${options.environment.rootDns}`;
|
|
||||||
const monacoCdnDynamicBasePath: `https://${string}/` = `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoEditorVersion}/`;
|
const monacoCdnDynamicBasePath: `https://${string}/` = `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoEditorVersion}/`;
|
||||||
const monacoCdnStaticBasePath: `https://${string}/` = `https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/`;
|
const monacoCdnStaticBasePath: `https://${string}/` = `https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/`;
|
||||||
const crispHost = 'client.crisp.chat';
|
const crispHost = 'client.crisp.chat';
|
||||||
|
|
@ -44,7 +43,6 @@ export function deployCloudFlareSecurityTransform(options: {
|
||||||
crispHost,
|
crispHost,
|
||||||
stripeHost,
|
stripeHost,
|
||||||
gtmHost,
|
gtmHost,
|
||||||
labHost,
|
|
||||||
'settings.crisp.chat',
|
'settings.crisp.chat',
|
||||||
'*.ingest.sentry.io',
|
'*.ingest.sentry.io',
|
||||||
'wss://client.relay.crisp.chat',
|
'wss://client.relay.crisp.chat',
|
||||||
|
|
@ -57,7 +55,6 @@ export function deployCloudFlareSecurityTransform(options: {
|
||||||
const contentSecurityPolicy = `
|
const contentSecurityPolicy = `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
frame-src ${stripeHost} https://game.crisp.chat;
|
frame-src ${stripeHost} https://game.crisp.chat;
|
||||||
worker-src 'self' blob: ${labHost};
|
|
||||||
style-src 'self' 'unsafe-inline' ${crispHost} fonts.googleapis.com rsms.me ${monacoCdnDynamicBasePath} ${monacoCdnStaticBasePath};
|
style-src 'self' 'unsafe-inline' ${crispHost} fonts.googleapis.com rsms.me ${monacoCdnDynamicBasePath} ${monacoCdnStaticBasePath};
|
||||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' {DYNAMIC_HOST_PLACEHOLDER} ${monacoCdnDynamicBasePath} ${monacoCdnStaticBasePath} ${cspHosts};
|
script-src 'self' 'unsafe-eval' 'unsafe-inline' {DYNAMIC_HOST_PLACEHOLDER} ${monacoCdnDynamicBasePath} ${monacoCdnStaticBasePath} ${cspHosts};
|
||||||
connect-src 'self' * {DYNAMIC_HOST_PLACEHOLDER} ${cspHosts};
|
connect-src 'self' * {DYNAMIC_HOST_PLACEHOLDER} ${cspHosts};
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,5 @@ export function deployProxy({
|
||||||
service: usage.service,
|
service: usage.service,
|
||||||
retriable: true,
|
retriable: true,
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
.get();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,67 @@ export class Proxy {
|
||||||
private staticIp?: { address?: string; aksReservedIpResourceGroup?: string },
|
private staticIp?: { address?: string; aksReservedIpResourceGroup?: string },
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
registerInternalProxy(
|
||||||
|
dnsRecord: string,
|
||||||
|
route: {
|
||||||
|
path: string;
|
||||||
|
service: k8s.core.v1.Service;
|
||||||
|
host: string;
|
||||||
|
customRewrite: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const cert = new k8s.apiextensions.CustomResource(`cert-${dnsRecord}`, {
|
||||||
|
apiVersion: 'cert-manager.io/v1',
|
||||||
|
kind: 'Certificate',
|
||||||
|
metadata: {
|
||||||
|
name: dnsRecord,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
commonName: dnsRecord,
|
||||||
|
dnsNames: [dnsRecord],
|
||||||
|
issuerRef: {
|
||||||
|
name: this.tlsSecretName,
|
||||||
|
kind: 'ClusterIssuer',
|
||||||
|
},
|
||||||
|
secretName: dnsRecord,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
new k8s.apiextensions.CustomResource(
|
||||||
|
`internal-proxy-${dnsRecord}`,
|
||||||
|
{
|
||||||
|
apiVersion: 'projectcontour.io/v1',
|
||||||
|
kind: 'HTTPProxy',
|
||||||
|
metadata: {
|
||||||
|
name: `internal-proxy-metadata-${dnsRecord}`,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
virtualhost: {
|
||||||
|
fqdn: route.host,
|
||||||
|
tls: {
|
||||||
|
secretName: dnsRecord,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
conditions: [{ prefix: route.path }],
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
name: route.service.metadata.name,
|
||||||
|
port: route.service.spec.ports[0].port,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pathRewritePolicy: {
|
||||||
|
replacePrefix: [{ prefix: route.path, replacement: route.customRewrite }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dependsOn: [cert, this.lbService!] },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
registerService(
|
registerService(
|
||||||
dns: { record: string; apex?: boolean },
|
dns: { record: string; apex?: boolean },
|
||||||
routes: {
|
routes: {
|
||||||
|
|
@ -29,7 +90,7 @@ export class Proxy {
|
||||||
withWwwDomain?: boolean;
|
withWwwDomain?: boolean;
|
||||||
// https://projectcontour.io/docs/1.29/config/rate-limiting/#local-rate-limiting
|
// https://projectcontour.io/docs/1.29/config/rate-limiting/#local-rate-limiting
|
||||||
rateLimit?: {
|
rateLimit?: {
|
||||||
// Max amount of request allowed with the "unit" paramter.
|
// Max amount of request allowed with the "unit" parameter.
|
||||||
maxRequests: number;
|
maxRequests: number;
|
||||||
unit: 'second' | 'minute' | 'hour';
|
unit: 'second' | 'minute' | 'hour';
|
||||||
// defining the number of requests above the baseline rate that are allowed in a short period of time.
|
// defining the number of requests above the baseline rate that are allowed in a short period of time.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
export HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret
|
|
||||||
export HIVE_EMAIL_FROM=no-reply@graphql-hive.com
|
|
||||||
export HIVE_APP_BASE_URL=http://localhost:8080
|
|
||||||
export SUPERTOKENS_API_KEY=wowverysecuremuchsecret
|
|
||||||
export CLICKHOUSE_USER=clickhouse
|
|
||||||
export CLICKHOUSE_PASSWORD=wowverysecuremuchsecret
|
|
||||||
export REDIS_PASSWORD=wowverysecuremuchsecret
|
|
||||||
export POSTGRES_PASSWORD=postgres
|
|
||||||
export POSTGRES_USER=postgres
|
|
||||||
export POSTGRES_DB=registry
|
|
||||||
export MINIO_ROOT_USER=minioadmin
|
|
||||||
export MINIO_ROOT_PASSWORD=minioadmin
|
|
||||||
export CDN_AUTH_PRIVATE_KEY=6b4721a99bd2ef6c00ce4328f34d95d7
|
|
||||||
|
|
@ -34,5 +34,9 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- 'stack'
|
- 'stack'
|
||||||
|
|
||||||
|
supertokens:
|
||||||
|
ports:
|
||||||
|
- '3567:3567'
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
stack: {}
|
stack: {}
|
||||||
|
|
|
||||||
|
|
@ -68,29 +68,18 @@ To run integration tests locally, from the pre-build Docker image, follow:
|
||||||
e2e Tests are based on Cypress, and matches files that ends with `.cy.ts`. The tests flow runs from
|
e2e Tests are based on Cypress, and matches files that ends with `.cy.ts`. The tests flow runs from
|
||||||
a pre-build Docker image.
|
a pre-build Docker image.
|
||||||
|
|
||||||
#### Running from Source Code
|
#### Running on built Docker images from source code
|
||||||
|
|
||||||
To run e2e tests locally, from the local source code, follow:
|
To run e2e tests locally, from the local source code, follow:
|
||||||
|
|
||||||
1. Make sure you have Docker installed. If you are having issues, try to run `docker system prune`
|
1. Make sure you have Docker installed. If you are having issues, try to run `docker system prune`
|
||||||
to clean the Docker caches.
|
to clean the Docker caches.
|
||||||
2. Install all deps: `pnpm i`
|
2. Install all deps: `pnpm i`
|
||||||
3. Generate types: `pnpm graphql:generate`
|
3. Move into the `cypress` folder (`cd cypress`)
|
||||||
4. Build source code: `pnpm build`
|
4. Run `./local.sh` for building the project and starting the Docker containers
|
||||||
5. Set env vars:
|
5. Follow the output instruction from the script for starting the tests
|
||||||
```bash
|
|
||||||
export COMMIT_SHA="local"
|
|
||||||
export RELEASE="local"
|
|
||||||
export BRANCH_NAME="local"
|
|
||||||
export BUILD_TYPE=""
|
|
||||||
export DOCKER_TAG=":local"
|
|
||||||
```
|
|
||||||
6. Compile a local Docker image by running: `docker buildx bake -f docker/docker.hcl build --load`
|
|
||||||
7. Run the e2e environment, by running:
|
|
||||||
`docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env up -d --wait`
|
|
||||||
8. Run Cypress: `pnpm test:e2e`
|
|
||||||
|
|
||||||
#### Running from Pre-Built Docker Image
|
#### Running from pre-built Docker image
|
||||||
|
|
||||||
To run integration tests locally, from the pre-build Docker image, follow:
|
To run integration tests locally, from the pre-build Docker image, follow:
|
||||||
|
|
||||||
|
|
@ -105,7 +94,13 @@ To run integration tests locally, from the pre-build Docker image, follow:
|
||||||
export DOCKER_TAG=":IMAGE_TAG_HERE"
|
export DOCKER_TAG=":IMAGE_TAG_HERE"
|
||||||
```
|
```
|
||||||
6. Run the e2e environment, by running:
|
6. Run the e2e environment, by running:
|
||||||
`docker compose -f ./docker/docker-compose.community.yml --env-file ./integration-tests/.env up -d --wait`
|
```
|
||||||
|
docker compose \
|
||||||
|
-f ./docker/docker-compose.community.yml \
|
||||||
|
-f ./docker/docker-compose.end2end.yml \
|
||||||
|
--env-file ./integration-tests/.env \
|
||||||
|
up -d --wait
|
||||||
|
```
|
||||||
7. Run Cypress: `pnpm test:e2e`
|
7. Run Cypress: `pnpm test:e2e`
|
||||||
|
|
||||||
#### Docker Compose Configuration
|
#### Docker Compose Configuration
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ const createSession = async (
|
||||||
*/
|
*/
|
||||||
return {
|
return {
|
||||||
access_token: data.accessToken.token,
|
access_token: data.accessToken.token,
|
||||||
|
refresh_token: data.refreshToken.token,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to create session:`, e);
|
console.warn(`Failed to create session:`, e);
|
||||||
|
|
@ -148,15 +149,17 @@ const tokenResponsePromise: {
|
||||||
[key: string]: Promise<z.TypeOf<typeof SignUpSignInUserResponseModel>> | null;
|
[key: string]: Promise<z.TypeOf<typeof SignUpSignInUserResponseModel>> | null;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
export function authenticate(email: string): Promise<{ access_token: string }>;
|
export function authenticate(
|
||||||
|
email: string,
|
||||||
|
): Promise<{ access_token: string; refresh_token: string }>;
|
||||||
export function authenticate(
|
export function authenticate(
|
||||||
email: string,
|
email: string,
|
||||||
oidcIntegrationId?: string,
|
oidcIntegrationId?: string,
|
||||||
): Promise<{ access_token: string }>;
|
): Promise<{ access_token: string; refresh_token: string }>;
|
||||||
export function authenticate(
|
export function authenticate(
|
||||||
email: string | string,
|
email: string | string,
|
||||||
oidcIntegrationId?: string,
|
oidcIntegrationId?: string,
|
||||||
): Promise<{ access_token: string }> {
|
): Promise<{ access_token: string; refresh_token: string }> {
|
||||||
if (!tokenResponsePromise[email]) {
|
if (!tokenResponsePromise[email]) {
|
||||||
tokenResponsePromise[email] = signUpUserViaEmail(email, password);
|
tokenResponsePromise[email] = signUpUserViaEmail(email, password);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,3 +202,22 @@ export const DeleteOperationMutation = graphql(`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
export const UpdatePreflightScriptMutation = graphql(`
|
||||||
|
mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) {
|
||||||
|
updatePreflightScript(input: $input) {
|
||||||
|
ok {
|
||||||
|
updatedTarget {
|
||||||
|
id
|
||||||
|
preflightScript {
|
||||||
|
id
|
||||||
|
sourceCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { RuleInstanceSeverityLevel, SchemaPolicyInput } from 'testkit/gql/graphql';
|
|
||||||
import { graphql } from './gql';
|
import { graphql } from './gql';
|
||||||
|
import { RuleInstanceSeverityLevel, SchemaPolicyInput } from './gql/graphql';
|
||||||
|
|
||||||
export const OrganizationAndProjectsWithSchemaPolicy = graphql(`
|
export const OrganizationAndProjectsWithSchemaPolicy = graphql(`
|
||||||
query OrganizationAndProjectsWithSchemaPolicy($organization: String!) {
|
query OrganizationAndProjectsWithSchemaPolicy($organization: String!) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import { humanId } from 'human-id';
|
import { humanId } from 'human-id';
|
||||||
import { createPool, sql } from 'slonik';
|
import { createPool, sql } from 'slonik';
|
||||||
import {
|
|
||||||
OrganizationAccessScope,
|
|
||||||
ProjectAccessScope,
|
|
||||||
ProjectType,
|
|
||||||
RegistryModel,
|
|
||||||
SchemaPolicyInput,
|
|
||||||
TargetAccessScope,
|
|
||||||
} from 'testkit/gql/graphql';
|
|
||||||
import type { Report } from '../../packages/libraries/core/src/client/usage.js';
|
import type { Report } from '../../packages/libraries/core/src/client/usage.js';
|
||||||
import { authenticate, userEmail } from './auth';
|
import { authenticate, userEmail } from './auth';
|
||||||
import {
|
import {
|
||||||
|
|
@ -17,6 +9,7 @@ import {
|
||||||
DeleteOperationMutation,
|
DeleteOperationMutation,
|
||||||
UpdateCollectionMutation,
|
UpdateCollectionMutation,
|
||||||
UpdateOperationMutation,
|
UpdateOperationMutation,
|
||||||
|
UpdatePreflightScriptMutation,
|
||||||
} from './collections';
|
} from './collections';
|
||||||
import { ensureEnv } from './env';
|
import { ensureEnv } from './env';
|
||||||
import {
|
import {
|
||||||
|
|
@ -57,21 +50,29 @@ import {
|
||||||
updateSchemaVersionStatus,
|
updateSchemaVersionStatus,
|
||||||
updateTargetValidationSettings,
|
updateTargetValidationSettings,
|
||||||
} from './flow';
|
} from './flow';
|
||||||
|
import {
|
||||||
|
OrganizationAccessScope,
|
||||||
|
ProjectAccessScope,
|
||||||
|
ProjectType,
|
||||||
|
RegistryModel,
|
||||||
|
SchemaPolicyInput,
|
||||||
|
TargetAccessScope,
|
||||||
|
} from './gql/graphql';
|
||||||
import { execute } from './graphql';
|
import { execute } from './graphql';
|
||||||
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
|
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
|
||||||
import { collect, CollectedOperation, legacyCollect } from './usage';
|
import { collect, CollectedOperation, legacyCollect } from './usage';
|
||||||
import { generateUnique } from './utils';
|
import { generateUnique } from './utils';
|
||||||
|
|
||||||
export function initSeed() {
|
export function initSeed() {
|
||||||
const pg = {
|
|
||||||
user: ensureEnv('POSTGRES_USER'),
|
|
||||||
password: ensureEnv('POSTGRES_PASSWORD'),
|
|
||||||
host: ensureEnv('POSTGRES_HOST'),
|
|
||||||
port: ensureEnv('POSTGRES_PORT'),
|
|
||||||
db: ensureEnv('POSTGRES_DB'),
|
|
||||||
};
|
|
||||||
|
|
||||||
function createConnectionPool() {
|
function createConnectionPool() {
|
||||||
|
const pg = {
|
||||||
|
user: ensureEnv('POSTGRES_USER'),
|
||||||
|
password: ensureEnv('POSTGRES_PASSWORD'),
|
||||||
|
host: ensureEnv('POSTGRES_HOST'),
|
||||||
|
port: ensureEnv('POSTGRES_PORT'),
|
||||||
|
db: ensureEnv('POSTGRES_DB'),
|
||||||
|
};
|
||||||
|
|
||||||
return createPool(
|
return createPool(
|
||||||
`postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`,
|
`postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`,
|
||||||
);
|
);
|
||||||
|
|
@ -87,15 +88,18 @@ export function initSeed() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
authenticate: authenticate,
|
authenticate,
|
||||||
generateEmail: () => userEmail(generateUnique()),
|
generateEmail: () => userEmail(generateUnique()),
|
||||||
async createOwner() {
|
async createOwner() {
|
||||||
const ownerEmail = userEmail(generateUnique());
|
const ownerEmail = userEmail(generateUnique());
|
||||||
const ownerToken = await authenticate(ownerEmail).then(r => r.access_token);
|
const auth = await authenticate(ownerEmail);
|
||||||
|
const ownerRefreshToken = auth.refresh_token;
|
||||||
|
const ownerToken = auth.access_token;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ownerEmail,
|
ownerEmail,
|
||||||
ownerToken,
|
ownerToken,
|
||||||
|
ownerRefreshToken,
|
||||||
async createOrg() {
|
async createOrg() {
|
||||||
const orgSlug = generateUnique();
|
const orgSlug = generateUnique();
|
||||||
const orgResult = await createOrganization({ slug: orgSlug }, ownerToken).then(r =>
|
const orgResult = await createOrganization({ slug: orgSlug }, ownerToken).then(r =>
|
||||||
|
|
@ -296,6 +300,30 @@ export function initSeed() {
|
||||||
|
|
||||||
return result.createDocumentCollection;
|
return result.createDocumentCollection;
|
||||||
},
|
},
|
||||||
|
async updatePreflightScript({
|
||||||
|
sourceCode,
|
||||||
|
token = ownerToken,
|
||||||
|
}: {
|
||||||
|
sourceCode: string;
|
||||||
|
token?: string;
|
||||||
|
}) {
|
||||||
|
const result = await execute({
|
||||||
|
document: UpdatePreflightScriptMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
selector: {
|
||||||
|
organizationSlug: organization.slug,
|
||||||
|
projectSlug: project.slug,
|
||||||
|
targetSlug: target.slug,
|
||||||
|
},
|
||||||
|
sourceCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authToken: token,
|
||||||
|
}).then(r => r.expectNoGraphQLErrors());
|
||||||
|
|
||||||
|
return result.updatePreflightScript;
|
||||||
|
},
|
||||||
async updateDocumentCollection({
|
async updateDocumentCollection({
|
||||||
collectionId,
|
collectionId,
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { ProjectType } from 'testkit/gql/graphql';
|
||||||
|
import { initSeed } from '../../../testkit/seed';
|
||||||
|
|
||||||
|
describe('Preflight Script', () => {
|
||||||
|
describe('CRUD', () => {
|
||||||
|
const rawJs = 'console.log("Hello World")';
|
||||||
|
|
||||||
|
it.concurrent('Update a Preflight Script', async () => {
|
||||||
|
const { updatePreflightScript } = await initSeed()
|
||||||
|
.createOwner()
|
||||||
|
.then(r => r.createOrg())
|
||||||
|
.then(r => r.createProject(ProjectType.Single));
|
||||||
|
|
||||||
|
const { error, ok } = await updatePreflightScript({ sourceCode: rawJs });
|
||||||
|
expect(error).toEqual(null);
|
||||||
|
expect(ok?.updatedTarget.preflightScript?.id).toBeDefined();
|
||||||
|
expect(ok?.updatedTarget.preflightScript?.sourceCode).toBe(rawJs);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Permissions Check', () => {
|
||||||
|
it('Prevent updating a Preflight Script without the write permission to the target', async () => {
|
||||||
|
const { updatePreflightScript, createTargetAccessToken } = await initSeed()
|
||||||
|
.createOwner()
|
||||||
|
.then(r => r.createOrg())
|
||||||
|
.then(r => r.createProject(ProjectType.Single));
|
||||||
|
|
||||||
|
const { secret: readOnlyToken } = await createTargetAccessToken({ mode: 'readOnly' });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
updatePreflightScript({ sourceCode: rawJs, token: readOnlyToken }),
|
||||||
|
).rejects.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
message: expect.stringContaining(
|
||||||
|
`No access (reason: "Missing permission for performing 'laboratory:modifyPreflightScript' on resource")`,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
"seed": "tsx scripts/seed-local-env.ts",
|
"seed": "tsx scripts/seed-local-env.ts",
|
||||||
"start": "pnpm run local:setup",
|
"start": "pnpm run local:setup",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run",
|
"test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run --browser chrome",
|
||||||
"test:e2e:open": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress open",
|
"test:e2e:open": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress open",
|
||||||
"test:integration": "cd integration-tests && pnpm test:integration",
|
"test:integration": "cd integration-tests && pnpm test:integration",
|
||||||
"typecheck": "pnpm run -r --filter '!hive' typecheck",
|
"typecheck": "pnpm run -r --filter '!hive' typecheck",
|
||||||
|
|
@ -119,9 +119,11 @@
|
||||||
"slonik@30.4.4": "patches/slonik@30.4.4.patch",
|
"slonik@30.4.4": "patches/slonik@30.4.4.patch",
|
||||||
"@oclif/core@3.26.6": "patches/@oclif__core@3.26.6.patch",
|
"@oclif/core@3.26.6": "patches/@oclif__core@3.26.6.patch",
|
||||||
"oclif@4.13.6": "patches/oclif@4.13.6.patch",
|
"oclif@4.13.6": "patches/oclif@4.13.6.patch",
|
||||||
"@graphiql/react@1.0.0-alpha.3": "patches/@graphiql__react@1.0.0-alpha.3.patch",
|
"graphiql": "patches/graphiql.patch",
|
||||||
|
"@graphiql/react": "patches/@graphiql__react.patch",
|
||||||
"countup.js": "patches/countup.js.patch",
|
"countup.js": "patches/countup.js.patch",
|
||||||
"@oclif/core@4.0.6": "patches/@oclif__core@4.0.6.patch"
|
"@oclif/core@4.0.6": "patches/@oclif__core@4.0.6.patch",
|
||||||
|
"@fastify/vite": "patches/@fastify__vite.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { type MigrationExecutor } from '../pg-migrator';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: '2024.12.27T00.00.00.create-preflight-scripts.ts',
|
||||||
|
run: ({ sql }) => sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS "document_preflight_scripts" (
|
||||||
|
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"source_code" text NOT NULL,
|
||||||
|
"target_id" uuid NOT NULL UNIQUE REFERENCES "targets"("id") ON DELETE CASCADE,
|
||||||
|
"created_by_user_id" uuid REFERENCES "users"("id") ON DELETE SET NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "document_preflight_scripts"
|
||||||
|
ADD CONSTRAINT "unique_target_id" UNIQUE ("target_id");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "document_preflight_scripts_target" ON "document_preflight_scripts" (
|
||||||
|
"target_id" ASC
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
} satisfies MigrationExecutor;
|
||||||
|
|
@ -149,5 +149,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
|
||||||
await import('./actions/2024.11.12T00-00-00.supertokens-9.3'),
|
await import('./actions/2024.11.12T00-00-00.supertokens-9.3'),
|
||||||
await import('./actions/2024.12.23T00-00-00.improve-version-index'),
|
await import('./actions/2024.12.23T00-00-00.improve-version-index'),
|
||||||
await import('./actions/2024.12.24T00-00-00.improve-version-index-2'),
|
await import('./actions/2024.12.24T00-00-00.improve-version-index-2'),
|
||||||
|
await import('./actions/2024.12.27T00.00.00.create-preflight-scripts'),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
"@hive/usage-common": "workspace:*",
|
"@hive/usage-common": "workspace:*",
|
||||||
"@hive/usage-ingestor": "workspace:*",
|
"@hive/usage-ingestor": "workspace:*",
|
||||||
"@hive/webhooks": "workspace:*",
|
"@hive/webhooks": "workspace:*",
|
||||||
|
"@nodesecure/i18n": "^4.0.1",
|
||||||
|
"@nodesecure/js-x-ray": "8.0.0",
|
||||||
"@octokit/app": "14.1.0",
|
"@octokit/app": "14.1.0",
|
||||||
"@octokit/core": "5.2.0",
|
"@octokit/core": "5.2.0",
|
||||||
"@octokit/plugin-retry": "6.1.0",
|
"@octokit/plugin-retry": "6.1.0",
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,12 @@ export const AuditLogModel = z.union([
|
||||||
updatedFields: z.string(),
|
updatedFields: z.string(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
eventType: z.literal('PREFLIGHT_SCRIPT_CHANGED'),
|
||||||
|
metadata: z.object({
|
||||||
|
scriptContents: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type AuditLogSchemaEvent = z.infer<typeof AuditLogModel>;
|
export type AuditLogSchemaEvent = z.infer<typeof AuditLogModel>;
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,7 @@ const actionDefinitions = {
|
||||||
'target:modifySettings': defaultTargetIdentity,
|
'target:modifySettings': defaultTargetIdentity,
|
||||||
'laboratory:describe': defaultTargetIdentity,
|
'laboratory:describe': defaultTargetIdentity,
|
||||||
'laboratory:modify': defaultTargetIdentity,
|
'laboratory:modify': defaultTargetIdentity,
|
||||||
|
'laboratory:modifyPreflightScript': defaultTargetIdentity,
|
||||||
'appDeployment:describe': defaultTargetIdentity,
|
'appDeployment:describe': defaultTargetIdentity,
|
||||||
'appDeployment:create': defaultAppDeploymentIdentity,
|
'appDeployment:create': defaultAppDeploymentIdentity,
|
||||||
'appDeployment:publish': defaultAppDeploymentIdentity,
|
'appDeployment:publish': defaultAppDeploymentIdentity,
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ function transformOrganizationMemberLegacyScopes(args: {
|
||||||
case TargetAccessScope.SETTINGS: {
|
case TargetAccessScope.SETTINGS: {
|
||||||
policies.push({
|
policies.push({
|
||||||
effect: 'allow',
|
effect: 'allow',
|
||||||
action: ['target:modifySettings'],
|
action: ['target:modifySettings', 'laboratory:modifyPreflightScript'],
|
||||||
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
|
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { createModule } from 'graphql-modules';
|
import { createModule } from 'graphql-modules';
|
||||||
|
import { PreflightScriptProvider } from './providers/preflight-script.provider';
|
||||||
import { resolvers } from './resolvers.generated';
|
import { resolvers } from './resolvers.generated';
|
||||||
import typeDefs from './module.graphql';
|
import typeDefs from './module.graphql';
|
||||||
|
|
||||||
|
|
@ -7,5 +8,5 @@ export const labModule = createModule({
|
||||||
dirname: __dirname,
|
dirname: __dirname,
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
providers: [],
|
providers: [PreflightScriptProvider],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,41 @@ export default gql`
|
||||||
schema: String!
|
schema: String!
|
||||||
mocks: JSON
|
mocks: JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreflightScript {
|
||||||
|
id: ID!
|
||||||
|
sourceCode: String!
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdatePreflightScriptInput {
|
||||||
|
selector: TargetSelectorInput!
|
||||||
|
sourceCode: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
updatePreflightScript(input: UpdatePreflightScriptInput!): PreflightScriptResult!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
@oneOf
|
||||||
|
"""
|
||||||
|
type PreflightScriptResult {
|
||||||
|
ok: PreflightScriptOk
|
||||||
|
error: PreflightScriptError
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreflightScriptOk {
|
||||||
|
preflightScript: PreflightScript!
|
||||||
|
updatedTarget: Target!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreflightScriptError implements Error {
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Target {
|
||||||
|
preflightScript: PreflightScript
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||||
|
import { sql, type DatabasePool } from 'slonik';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { getLocalLang, getTokenSync } from '@nodesecure/i18n';
|
||||||
|
import * as jsxray from '@nodesecure/js-x-ray';
|
||||||
|
import type { Target } from '../../../shared/entities';
|
||||||
|
import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder';
|
||||||
|
import { Session } from '../../auth/lib/authz';
|
||||||
|
import { IdTranslator } from '../../shared/providers/id-translator';
|
||||||
|
import { Logger } from '../../shared/providers/logger';
|
||||||
|
import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool';
|
||||||
|
import { Storage } from '../../shared/providers/storage';
|
||||||
|
|
||||||
|
const SourceCodeModel = z.string().max(5_000);
|
||||||
|
|
||||||
|
const UpdatePreflightScriptModel = z.strictObject({
|
||||||
|
// Use validation only on insertion
|
||||||
|
sourceCode: SourceCodeModel.superRefine((val, ctx) => {
|
||||||
|
try {
|
||||||
|
const { warnings } = scanner.analyse(val);
|
||||||
|
for (const warning of warnings) {
|
||||||
|
const message = getTokenSync(jsxray.warnings[warning.kind].i18n);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PreflightScriptModel = z.strictObject({
|
||||||
|
id: z.string(),
|
||||||
|
sourceCode: SourceCodeModel,
|
||||||
|
targetId: z.string(),
|
||||||
|
createdByUserId: z.union([z.string(), z.null()]),
|
||||||
|
createdAt: z.string(),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PreflightScript = z.TypeOf<typeof PreflightScriptModel>;
|
||||||
|
|
||||||
|
const scanner = new jsxray.AstAnalyser();
|
||||||
|
await getLocalLang();
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
global: true,
|
||||||
|
scope: Scope.Operation,
|
||||||
|
})
|
||||||
|
export class PreflightScriptProvider {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
logger: Logger,
|
||||||
|
private storage: Storage,
|
||||||
|
private session: Session,
|
||||||
|
private idTranslator: IdTranslator,
|
||||||
|
private auditLogs: AuditLogRecorder,
|
||||||
|
@Inject(PG_POOL_CONFIG) private pool: DatabasePool,
|
||||||
|
) {
|
||||||
|
this.logger = logger.child({ source: 'PreflightScriptProvider' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPreflightScript(targetId: string) {
|
||||||
|
const result = await this.pool.maybeOne<unknown>(sql`/* getPreflightScript */
|
||||||
|
SELECT
|
||||||
|
"id"
|
||||||
|
, "source_code" AS "sourceCode"
|
||||||
|
, "target_id" AS "targetId"
|
||||||
|
, "created_by_user_id" AS "createdByUserId"
|
||||||
|
, to_json("created_at") AS "createdAt"
|
||||||
|
, to_json("updated_at") AS "updatedAt"
|
||||||
|
FROM
|
||||||
|
"document_preflight_scripts"
|
||||||
|
WHERE
|
||||||
|
"target_id" = ${targetId}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreflightScriptModel.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePreflightScript(args: {
|
||||||
|
selector: {
|
||||||
|
organizationSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
targetSlug: string;
|
||||||
|
};
|
||||||
|
sourceCode: string;
|
||||||
|
}): Promise<
|
||||||
|
| {
|
||||||
|
error: { message: string };
|
||||||
|
ok?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
error?: never;
|
||||||
|
ok: {
|
||||||
|
preflightScript: PreflightScript;
|
||||||
|
updatedTarget: Target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
const [organizationId, projectId, targetId] = await Promise.all([
|
||||||
|
this.idTranslator.translateOrganizationId(args.selector),
|
||||||
|
this.idTranslator.translateProjectId(args.selector),
|
||||||
|
this.idTranslator.translateTargetId(args.selector),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.session.assertPerformAction({
|
||||||
|
action: 'laboratory:modifyPreflightScript',
|
||||||
|
organizationId,
|
||||||
|
params: {
|
||||||
|
organizationId,
|
||||||
|
projectId,
|
||||||
|
targetId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationResult = UpdatePreflightScriptModel.safeParse({ sourceCode: args.sourceCode });
|
||||||
|
|
||||||
|
if (validationResult.error) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: validationResult.error.errors[0].message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser = await this.session.getViewer();
|
||||||
|
const result = await this.pool.maybeOne(sql`/* createPreflightScript */
|
||||||
|
INSERT INTO "document_preflight_scripts" (
|
||||||
|
"source_code"
|
||||||
|
, "target_id"
|
||||||
|
, "created_by_user_id")
|
||||||
|
VALUES (
|
||||||
|
${validationResult.data.sourceCode}
|
||||||
|
, ${targetId}
|
||||||
|
, ${currentUser.id}
|
||||||
|
)
|
||||||
|
ON CONFLICT ("target_id")
|
||||||
|
DO UPDATE
|
||||||
|
SET
|
||||||
|
"source_code" = EXCLUDED."source_code"
|
||||||
|
, "updated_at" = NOW()
|
||||||
|
RETURNING
|
||||||
|
"id"
|
||||||
|
, "source_code" AS "sourceCode"
|
||||||
|
, "target_id" AS "targetId"
|
||||||
|
, "created_by_user_id" AS "createdByUserId"
|
||||||
|
, to_json("created_at") AS "createdAt"
|
||||||
|
, to_json("updated_at") AS "updatedAt"
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: 'No preflight script found',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { data: preflightScript, error } = PreflightScriptModel.safeParse(result);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: error.errors[0].message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.auditLogs.record({
|
||||||
|
eventType: 'PREFLIGHT_SCRIPT_CHANGED',
|
||||||
|
organizationId,
|
||||||
|
metadata: {
|
||||||
|
scriptContents: preflightScript.sourceCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedTarget = await this.storage.getTarget({
|
||||||
|
organizationId,
|
||||||
|
projectId,
|
||||||
|
targetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: {
|
||||||
|
preflightScript,
|
||||||
|
updatedTarget,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { MutationResolvers } from '../../../../__generated__/types';
|
||||||
|
import { PreflightScriptProvider } from '../../providers/preflight-script.provider';
|
||||||
|
|
||||||
|
export const updatePreflightScript: NonNullable<
|
||||||
|
MutationResolvers['updatePreflightScript']
|
||||||
|
> = async (_parent, args, { injector }) => {
|
||||||
|
const result = await injector.get(PreflightScriptProvider).updatePreflightScript({
|
||||||
|
selector: args.input.selector,
|
||||||
|
sourceCode: args.input.sourceCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return {
|
||||||
|
error: result.error,
|
||||||
|
ok: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: result.ok,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
16
packages/services/api/src/modules/lab/resolvers/Target.ts
Normal file
16
packages/services/api/src/modules/lab/resolvers/Target.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { TargetResolvers } from '../../../__generated__/types';
|
||||||
|
import { PreflightScriptProvider } from '../providers/preflight-script.provider';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Note: This object type is generated because "TargetMapper" is declared. This is to ensure runtime safety.
|
||||||
|
*
|
||||||
|
* When a mapper is used, it is possible to hit runtime errors in some scenarios:
|
||||||
|
* - given a field name, the schema type's field type does not match mapper's field type
|
||||||
|
* - or a schema type's field does not exist in the mapper's fields
|
||||||
|
*
|
||||||
|
* If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config.
|
||||||
|
*/
|
||||||
|
export const Target: Pick<TargetResolvers, 'preflightScript' | '__isTypeOf'> = {
|
||||||
|
preflightScript: (parent, _args, { injector }) =>
|
||||||
|
injector.get(PreflightScriptProvider).getPreflightScript(parent.id),
|
||||||
|
};
|
||||||
|
|
@ -240,6 +240,15 @@ export interface DocumentCollection {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreflightScript {
|
||||||
|
id: string;
|
||||||
|
sourceCode: string;
|
||||||
|
targetId: string;
|
||||||
|
createdByUserId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type PaginatedDocumentCollections = Readonly<{
|
export type PaginatedDocumentCollections = Readonly<{
|
||||||
edges: ReadonlyArray<{
|
edges: ReadonlyArray<{
|
||||||
node: DocumentCollection;
|
node: DocumentCollection;
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,15 @@ export interface document_collections {
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface document_preflight_scripts {
|
||||||
|
created_at: Date;
|
||||||
|
created_by_user_id: string | null;
|
||||||
|
id: string;
|
||||||
|
source_code: string;
|
||||||
|
target_id: string;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface migration {
|
export interface migration {
|
||||||
date: Date;
|
date: Date;
|
||||||
hash: string;
|
hash: string;
|
||||||
|
|
@ -417,6 +426,7 @@ export interface DBTables {
|
||||||
contracts: contracts;
|
contracts: contracts;
|
||||||
document_collection_documents: document_collection_documents;
|
document_collection_documents: document_collection_documents;
|
||||||
document_collections: document_collections;
|
document_collections: document_collections;
|
||||||
|
document_preflight_scripts: document_preflight_scripts;
|
||||||
migration: migration;
|
migration: migration;
|
||||||
oidc_integrations: oidc_integrations;
|
oidc_integrations: oidc_integrations;
|
||||||
organization_invitations: organization_invitations;
|
organization_invitations: organization_invitations;
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
"@date-fns/utc": "2.1.0",
|
"@date-fns/utc": "2.1.0",
|
||||||
"@fastify/cors": "9.0.1",
|
"@fastify/cors": "9.0.1",
|
||||||
"@fastify/static": "7.0.4",
|
"@fastify/static": "7.0.4",
|
||||||
"@fastify/vite": "6.0.7",
|
"@fastify/vite": "6.0.6",
|
||||||
"@graphiql/plugin-explorer": "4.0.0-alpha.2",
|
"@graphiql/plugin-explorer": "4.0.0-alpha.2",
|
||||||
"@graphiql/react": "1.0.0-alpha.3",
|
"@graphiql/react": "1.0.0-alpha.4",
|
||||||
"@graphiql/toolkit": "0.9.1",
|
"@graphiql/toolkit": "0.9.1",
|
||||||
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
|
||||||
"@graphql-tools/mock": "9.0.6",
|
"@graphql-tools/mock": "9.0.6",
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
"@theguild/editor": "1.2.5",
|
"@theguild/editor": "1.2.5",
|
||||||
"@trpc/client": "10.45.2",
|
"@trpc/client": "10.45.2",
|
||||||
"@trpc/server": "10.45.2",
|
"@trpc/server": "10.45.2",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/dompurify": "3.2.0",
|
"@types/dompurify": "3.2.0",
|
||||||
"@types/js-cookie": "3.0.6",
|
"@types/js-cookie": "3.0.6",
|
||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.18",
|
||||||
|
|
@ -81,6 +82,7 @@
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "0.2.1",
|
"cmdk": "0.2.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"dompurify": "3.2.3",
|
"dompurify": "3.2.3",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
|
|
@ -88,8 +90,8 @@
|
||||||
"echarts-for-react": "3.0.2",
|
"echarts-for-react": "3.0.2",
|
||||||
"fastify": "4.29.0",
|
"fastify": "4.29.0",
|
||||||
"formik": "2.4.6",
|
"formik": "2.4.6",
|
||||||
"framer-motion": "11.15.0",
|
"framer-motion": "11.11.17",
|
||||||
"graphiql": "4.0.0-alpha.4",
|
"graphiql": "4.0.0-alpha.5",
|
||||||
"graphql": "16.9.0",
|
"graphql": "16.9.0",
|
||||||
"graphql-sse": "2.5.3",
|
"graphql-sse": "2.5.3",
|
||||||
"immer": "10.1.1",
|
"immer": "10.1.1",
|
||||||
|
|
|
||||||
13
packages/web/app/preflight-worker-embed.html
Normal file
13
packages/web/app/preflight-worker-embed.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="https://www.youtube.com/watch?v=CMNry4PE93Y" rel="nofollow">I like turtles</a>
|
||||||
|
<a href="https://www.youtube.com/watch?v=XOi2jFIhZhA" rel="nofollow">Wheatherboi</a>
|
||||||
|
|
||||||
|
<script type="module" src="./src/lib/preflight-sandbox/preflight-worker-embed.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -104,6 +104,7 @@ export const TargetLaboratoryPageQuery = graphql(`
|
||||||
}
|
}
|
||||||
viewerCanViewLaboratory
|
viewerCanViewLaboratory
|
||||||
viewerCanModifyLaboratory
|
viewerCanModifyLaboratory
|
||||||
|
...PreflightScript_TargetFragment
|
||||||
}
|
}
|
||||||
...Laboratory_IsCDNEnabledFragment
|
...Laboratory_IsCDNEnabledFragment
|
||||||
}
|
}
|
||||||
|
|
@ -123,11 +124,9 @@ export const operationCollectionsPlugin: GraphiQLPlugin = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Content() {
|
export function Content() {
|
||||||
const { organizationSlug, projectSlug, targetSlug } = useParams({ strict: false }) as {
|
const { organizationSlug, projectSlug, targetSlug } = useParams({
|
||||||
organizationSlug: string;
|
from: '/authenticated/$organizationSlug/$projectSlug/$targetSlug',
|
||||||
projectSlug: string;
|
});
|
||||||
targetSlug: string;
|
|
||||||
};
|
|
||||||
const [query] = useQuery({
|
const [query] = useQuery({
|
||||||
query: TargetLaboratoryPageQuery,
|
query: TargetLaboratoryPageQuery,
|
||||||
variables: {
|
variables: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* List all variables that we want to allow users to use inside their scripts
|
||||||
|
*
|
||||||
|
* initial list comes from https://github.com/postmanlabs/uniscope/blob/develop/lib/allowed-globals.js
|
||||||
|
*/
|
||||||
|
export const ALLOWED_GLOBALS = new Set([
|
||||||
|
'Array',
|
||||||
|
'Atomics',
|
||||||
|
'BigInt',
|
||||||
|
'Boolean',
|
||||||
|
'DataView',
|
||||||
|
'Date',
|
||||||
|
'Error',
|
||||||
|
'EvalError',
|
||||||
|
'Infinity',
|
||||||
|
'JSON',
|
||||||
|
'Map',
|
||||||
|
'Math',
|
||||||
|
'NaN',
|
||||||
|
'Number',
|
||||||
|
'Object',
|
||||||
|
'Promise',
|
||||||
|
'Proxy',
|
||||||
|
'RangeError',
|
||||||
|
'ReferenceError',
|
||||||
|
'Reflect',
|
||||||
|
'RegExp',
|
||||||
|
'Set',
|
||||||
|
'String',
|
||||||
|
'Symbol',
|
||||||
|
'SyntaxError',
|
||||||
|
'TypeError',
|
||||||
|
'URIError',
|
||||||
|
'WeakMap',
|
||||||
|
'WeakSet',
|
||||||
|
'decodeURI',
|
||||||
|
'decodeURIComponent',
|
||||||
|
'encodeURI',
|
||||||
|
'encodeURIComponent',
|
||||||
|
'escape',
|
||||||
|
'isFinite',
|
||||||
|
'isNaN',
|
||||||
|
'parseFloat',
|
||||||
|
'parseInt',
|
||||||
|
'undefined',
|
||||||
|
'unescape',
|
||||||
|
// More global variables
|
||||||
|
'btoa',
|
||||||
|
'atob',
|
||||||
|
'fetch',
|
||||||
|
'setTimeout',
|
||||||
|
// We aren't allowing access to window.console, but we need to "allow" it
|
||||||
|
// here so a second argument isn't added for it below.
|
||||||
|
'console',
|
||||||
|
]);
|
||||||
672
packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx
Normal file
672
packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx
Normal file
|
|
@ -0,0 +1,672 @@
|
||||||
|
import {
|
||||||
|
ComponentPropsWithoutRef,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import type { editor } from 'monaco-editor';
|
||||||
|
import { useMutation } from 'urql';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Subtitle, Title } from '@/components/ui/page';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||||
|
import { useLocalStorage, useToggle } from '@/lib/hooks';
|
||||||
|
import { GraphiQLPlugin } from '@graphiql/react';
|
||||||
|
import { Editor as MonacoEditor, OnMount } from '@monaco-editor/react';
|
||||||
|
import {
|
||||||
|
Cross2Icon,
|
||||||
|
CrossCircledIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
InfoCircledIcon,
|
||||||
|
Pencil1Icon,
|
||||||
|
TriangleRightIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import { useParams } from '@tanstack/react-router';
|
||||||
|
import type { LogMessage } from './preflight-script-worker';
|
||||||
|
|
||||||
|
export const preflightScriptPlugin: GraphiQLPlugin = {
|
||||||
|
icon: () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="16"
|
||||||
|
>
|
||||||
|
<path d="M136 160h40" />
|
||||||
|
<path d="m80 96 40 32-40 32" />
|
||||||
|
<rect width="192" height="160" x="32" y="48" rx="8.5" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
title: 'Preflight Script',
|
||||||
|
content: PreflightScriptContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = {
|
||||||
|
monaco: clsx('*:bg-[#10151f]'),
|
||||||
|
monacoMini: clsx('h-32 *:rounded-md *:bg-[#10151f]'),
|
||||||
|
icon: clsx('absolute -left-5 top-px'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharedMonacoProps = {
|
||||||
|
theme: 'vs-dark',
|
||||||
|
className: classes.monaco,
|
||||||
|
options: {
|
||||||
|
minimap: { enabled: false },
|
||||||
|
padding: {
|
||||||
|
top: 10,
|
||||||
|
},
|
||||||
|
scrollbar: {
|
||||||
|
horizontalScrollbarSize: 6,
|
||||||
|
verticalScrollbarSize: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies ComponentPropsWithoutRef<typeof MonacoEditor>;
|
||||||
|
|
||||||
|
const monacoProps = {
|
||||||
|
env: {
|
||||||
|
...sharedMonacoProps,
|
||||||
|
defaultLanguage: 'json',
|
||||||
|
options: {
|
||||||
|
...sharedMonacoProps.options,
|
||||||
|
lineNumbers: 'off',
|
||||||
|
tabSize: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
script: {
|
||||||
|
...sharedMonacoProps,
|
||||||
|
theme: 'vs-dark',
|
||||||
|
defaultLanguage: 'javascript',
|
||||||
|
options: {
|
||||||
|
...sharedMonacoProps.options,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Record<'script' | 'env', ComponentPropsWithoutRef<typeof MonacoEditor>>;
|
||||||
|
|
||||||
|
type PayloadLog = { type: 'log'; log: string };
|
||||||
|
type PayloadError = { type: 'error'; error: Error };
|
||||||
|
type PayloadResult = { type: 'result'; environmentVariables: Record<string, unknown> };
|
||||||
|
type PayloadReady = { type: 'ready' };
|
||||||
|
|
||||||
|
type WorkerMessagePayload = PayloadResult | PayloadLog | PayloadError | PayloadReady;
|
||||||
|
|
||||||
|
const UpdatePreflightScriptMutation = graphql(`
|
||||||
|
mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) {
|
||||||
|
updatePreflightScript(input: $input) {
|
||||||
|
ok {
|
||||||
|
updatedTarget {
|
||||||
|
id
|
||||||
|
preflightScript {
|
||||||
|
id
|
||||||
|
sourceCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const PreflightScript_TargetFragment = graphql(`
|
||||||
|
fragment PreflightScript_TargetFragment on Target {
|
||||||
|
id
|
||||||
|
preflightScript {
|
||||||
|
id
|
||||||
|
sourceCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
type LogRecord = LogMessage | { type: 'separator' };
|
||||||
|
|
||||||
|
function safeParseJSON(str: string): Record<string, unknown> | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum PreflightWorkerState {
|
||||||
|
running,
|
||||||
|
ready,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreflightScript(args: {
|
||||||
|
target: FragmentType<typeof PreflightScript_TargetFragment> | null;
|
||||||
|
}) {
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
|
const target = useFragment(PreflightScript_TargetFragment, args.target);
|
||||||
|
const [isPreflightScriptEnabled, setIsPreflightScriptEnabled] = useLocalStorage(
|
||||||
|
'hive:laboratory:isPreflightScriptEnabled',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const [environmentVariables, setEnvironmentVariables] = useLocalStorage(
|
||||||
|
'hive:laboratory:environment',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
const latestEnvironmentVariablesRef = useRef(environmentVariables);
|
||||||
|
useEffect(() => {
|
||||||
|
latestEnvironmentVariablesRef.current = environmentVariables;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [state, setState] = useState<PreflightWorkerState>(PreflightWorkerState.ready);
|
||||||
|
const [logs, setLogs] = useState<LogRecord[]>([]);
|
||||||
|
|
||||||
|
const currentRun = useRef<null | Function>(null);
|
||||||
|
|
||||||
|
async function execute(script = target?.preflightScript?.sourceCode ?? '', isPreview = false) {
|
||||||
|
if (isPreview === false && !isPreflightScriptEnabled) {
|
||||||
|
return safeParseJSON(latestEnvironmentVariablesRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
setState(PreflightWorkerState.running);
|
||||||
|
const now = Date.now();
|
||||||
|
setLogs(prev => [...prev, '> Start running script']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contentWindow = iframeRef.current?.contentWindow;
|
||||||
|
|
||||||
|
if (!contentWindow) {
|
||||||
|
throw new Error('Could not load iframe embed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
contentWindow.postMessage(
|
||||||
|
{
|
||||||
|
type: 'run',
|
||||||
|
id,
|
||||||
|
script,
|
||||||
|
environmentVariables: (environmentVariables && safeParseJSON(environmentVariables)) || {},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFinishedD = Promise.withResolvers<void>();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-inner-declarations
|
||||||
|
function eventHandler(ev: MessageEvent<WorkerMessagePayload>) {
|
||||||
|
if (ev.data.type === 'result') {
|
||||||
|
const mergedEnvironmentVariables = JSON.stringify(
|
||||||
|
{
|
||||||
|
...safeParseJSON(latestEnvironmentVariablesRef.current),
|
||||||
|
...ev.data.environmentVariables,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
setEnvironmentVariables(mergedEnvironmentVariables);
|
||||||
|
latestEnvironmentVariablesRef.current = mergedEnvironmentVariables;
|
||||||
|
setLogs(logs => [
|
||||||
|
...logs,
|
||||||
|
`> End running script. Done in ${(Date.now() - now) / 1000}s`,
|
||||||
|
{
|
||||||
|
type: 'separator' as const,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
isFinishedD.resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.data.type === 'error') {
|
||||||
|
const error = ev.data.error;
|
||||||
|
setLogs(logs => [
|
||||||
|
...logs,
|
||||||
|
error,
|
||||||
|
'> Preflight script failed',
|
||||||
|
{
|
||||||
|
type: 'separator' as const,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
isFinishedD.resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.data.type === 'log') {
|
||||||
|
const log = ev.data.log;
|
||||||
|
setLogs(logs => [...logs, log]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', eventHandler);
|
||||||
|
currentRun.current = () => {
|
||||||
|
contentWindow.postMessage({
|
||||||
|
type: 'abort',
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
currentRun.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
await isFinishedD.promise;
|
||||||
|
window.removeEventListener('message', eventHandler);
|
||||||
|
|
||||||
|
setState(PreflightWorkerState.ready);
|
||||||
|
return safeParseJSON(latestEnvironmentVariablesRef.current);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setLogs(prev => [
|
||||||
|
...prev,
|
||||||
|
err,
|
||||||
|
'> Preflight script failed',
|
||||||
|
{
|
||||||
|
type: 'separator' as const,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setState(PreflightWorkerState.ready);
|
||||||
|
return safeParseJSON(latestEnvironmentVariablesRef.current);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function abort() {
|
||||||
|
currentRun.current?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminate worker when leaving laboratory
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
currentRun.current?.();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
execute,
|
||||||
|
abort,
|
||||||
|
isPreflightScriptEnabled,
|
||||||
|
setIsPreflightScriptEnabled,
|
||||||
|
script: target?.preflightScript?.sourceCode ?? '',
|
||||||
|
environmentVariables,
|
||||||
|
setEnvironmentVariables,
|
||||||
|
state,
|
||||||
|
logs,
|
||||||
|
clearLogs: () => setLogs([]),
|
||||||
|
iframeElement: (
|
||||||
|
<iframe
|
||||||
|
src="/__preflight-embed"
|
||||||
|
title="preflight-worker"
|
||||||
|
className="hidden"
|
||||||
|
/**
|
||||||
|
* In DEV we need to use "allow-same-origin", as otherwise the embed can not instantiate the webworker (which is loaded from an URL).
|
||||||
|
* In PROD the webworker is not
|
||||||
|
*/
|
||||||
|
sandbox={import.meta.env.DEV ? 'allow-scripts allow-same-origin' : 'allow-scripts'}
|
||||||
|
ref={iframeRef}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreflightScriptObject = ReturnType<typeof usePreflightScript>;
|
||||||
|
|
||||||
|
const PreflightScriptContext = createContext<PreflightScriptObject | null>(null);
|
||||||
|
export const PreflightScriptProvider = PreflightScriptContext.Provider;
|
||||||
|
|
||||||
|
function PreflightScriptContent() {
|
||||||
|
const preflightScript = useContext(PreflightScriptContext);
|
||||||
|
if (preflightScript === null) {
|
||||||
|
throw new Error('PreflightScriptContent used outside PreflightScriptContext.Provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [showModal, toggleShowModal] = useToggle();
|
||||||
|
const params = useParams({
|
||||||
|
from: '/authenticated/$organizationSlug/$projectSlug/$targetSlug',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [, mutate] = useMutation(UpdatePreflightScriptMutation);
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const handleScriptChange = useCallback(async (newValue = '') => {
|
||||||
|
const { data, error } = await mutate({
|
||||||
|
input: {
|
||||||
|
selector: params,
|
||||||
|
sourceCode: newValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const err = error || data?.updatePreflightScript?.error;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: err.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Update',
|
||||||
|
description: 'Preflight script has been updated successfully',
|
||||||
|
variant: 'default',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PreflightScriptModal
|
||||||
|
// to unmount on submit/close
|
||||||
|
key={String(showModal)}
|
||||||
|
isOpen={showModal}
|
||||||
|
toggle={toggleShowModal}
|
||||||
|
scriptValue={preflightScript.script}
|
||||||
|
executeScript={value =>
|
||||||
|
preflightScript.execute(value, true).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
state={preflightScript.state}
|
||||||
|
abortScriptRun={preflightScript.abort}
|
||||||
|
logs={preflightScript.logs}
|
||||||
|
clearLogs={preflightScript.clearLogs}
|
||||||
|
onScriptValueChange={handleScriptChange}
|
||||||
|
envValue={preflightScript.environmentVariables}
|
||||||
|
onEnvValueChange={preflightScript.setEnvironmentVariables}
|
||||||
|
/>
|
||||||
|
<div className="graphiql-doc-explorer-title flex items-center justify-between gap-4">
|
||||||
|
Preflight Script
|
||||||
|
<Button
|
||||||
|
variant="orangeLink"
|
||||||
|
size="icon-sm"
|
||||||
|
className="size-auto gap-1"
|
||||||
|
onClick={toggleShowModal}
|
||||||
|
data-cy="preflight-script-modal-button"
|
||||||
|
>
|
||||||
|
<Pencil1Icon className="shrink-0" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Subtitle>
|
||||||
|
This script is run before each operation submitted, e.g. for automated authentication.
|
||||||
|
</Subtitle>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Switch
|
||||||
|
checked={preflightScript.isPreflightScriptEnabled}
|
||||||
|
onCheckedChange={v => preflightScript.setIsPreflightScriptEnabled(v)}
|
||||||
|
className="my-4"
|
||||||
|
data-cy="toggle-preflight-script"
|
||||||
|
/>
|
||||||
|
<span className="w-6">{preflightScript.isPreflightScriptEnabled ? 'ON' : 'OFF'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{preflightScript.isPreflightScriptEnabled && (
|
||||||
|
<MonacoEditor
|
||||||
|
height={128}
|
||||||
|
value={preflightScript.script}
|
||||||
|
{...monacoProps.script}
|
||||||
|
className={classes.monacoMini}
|
||||||
|
wrapperProps={{
|
||||||
|
['data-cy']: 'preflight-script-editor-mini',
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
...monacoProps.script.options,
|
||||||
|
lineNumbers: 'off',
|
||||||
|
readOnly: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Title className="mt-6 flex items-center gap-2">
|
||||||
|
Environment variables{' '}
|
||||||
|
<Badge className="text-xs" variant="outline">
|
||||||
|
JSON
|
||||||
|
</Badge>
|
||||||
|
</Title>
|
||||||
|
<Subtitle>Define variables to use in your Headers</Subtitle>
|
||||||
|
<MonacoEditor
|
||||||
|
height={128}
|
||||||
|
value={preflightScript.environmentVariables}
|
||||||
|
onChange={value => preflightScript.setEnvironmentVariables(value ?? '')}
|
||||||
|
{...monacoProps.env}
|
||||||
|
className={classes.monacoMini}
|
||||||
|
wrapperProps={{
|
||||||
|
['data-cy']: 'env-editor-mini',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreflightScriptModal({
|
||||||
|
isOpen,
|
||||||
|
toggle,
|
||||||
|
scriptValue,
|
||||||
|
executeScript,
|
||||||
|
state,
|
||||||
|
abortScriptRun,
|
||||||
|
logs,
|
||||||
|
clearLogs,
|
||||||
|
onScriptValueChange,
|
||||||
|
envValue,
|
||||||
|
onEnvValueChange,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
scriptValue?: string;
|
||||||
|
executeScript: (script: string) => void;
|
||||||
|
state: PreflightWorkerState;
|
||||||
|
abortScriptRun: () => void;
|
||||||
|
logs: Array<LogRecord>;
|
||||||
|
clearLogs: () => void;
|
||||||
|
onScriptValueChange: (value: string) => void;
|
||||||
|
envValue: string;
|
||||||
|
onEnvValueChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
const scriptEditorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||||
|
const envEditorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||||
|
const consoleRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
const handleScriptEditorDidMount: OnMount = useCallback(editor => {
|
||||||
|
scriptEditorRef.current = editor;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEnvEditorDidMount: OnMount = useCallback(editor => {
|
||||||
|
envEditorRef.current = editor;
|
||||||
|
}, []);
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
onScriptValueChange(scriptEditorRef.current?.getValue() ?? '');
|
||||||
|
onEnvValueChange(envEditorRef.current?.getValue() ?? '');
|
||||||
|
toggle();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const consoleEl = consoleRef.current;
|
||||||
|
consoleEl?.scroll({ top: consoleEl.scrollHeight, behavior: 'smooth' });
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={open => {
|
||||||
|
if (!open) {
|
||||||
|
abortScriptRun();
|
||||||
|
}
|
||||||
|
toggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="w-11/12 max-w-[unset] xl:w-4/5"
|
||||||
|
onEscapeKeyDown={ev => {
|
||||||
|
// prevent pressing escape in monaco to close the modal
|
||||||
|
if (ev.target instanceof HTMLTextAreaElement) {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit your Preflight Script</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
This script will run in each user's browser and be stored in plain text on our servers.
|
||||||
|
Don't share any secrets here.
|
||||||
|
<br />
|
||||||
|
All team members can view the script and toggle it off when they need to.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid h-[60vh] grid-cols-2 [&_section]:grow">
|
||||||
|
<div className="mr-4 flex flex-col">
|
||||||
|
<div className="flex justify-between p-2">
|
||||||
|
<Title className="flex gap-2">
|
||||||
|
Script Editor
|
||||||
|
<Badge className="text-xs" variant="outline">
|
||||||
|
JavaScript
|
||||||
|
</Badge>
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
variant="orangeLink"
|
||||||
|
size="icon-sm"
|
||||||
|
className="size-auto gap-1"
|
||||||
|
onClick={() => {
|
||||||
|
if (state === PreflightWorkerState.running) {
|
||||||
|
abortScriptRun();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executeScript(scriptEditorRef.current?.getValue() ?? '');
|
||||||
|
}}
|
||||||
|
data-cy="run-preflight-script"
|
||||||
|
>
|
||||||
|
{state === PreflightWorkerState.running && (
|
||||||
|
<>
|
||||||
|
<Cross2Icon className="shrink-0" />
|
||||||
|
Stop Script
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{state === PreflightWorkerState.ready && (
|
||||||
|
<>
|
||||||
|
<TriangleRightIcon className="shrink-0" />
|
||||||
|
Run Script
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<MonacoEditor
|
||||||
|
value={scriptValue}
|
||||||
|
onMount={handleScriptEditorDidMount}
|
||||||
|
{...monacoProps.script}
|
||||||
|
options={{
|
||||||
|
...monacoProps.script.options,
|
||||||
|
wordWrap: 'wordWrapColumn',
|
||||||
|
}}
|
||||||
|
wrapperProps={{
|
||||||
|
['data-cy']: 'preflight-script-editor',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-[inherit] flex-col">
|
||||||
|
<div className="flex justify-between p-2">
|
||||||
|
<Title>Console Output</Title>
|
||||||
|
<Button
|
||||||
|
variant="orangeLink"
|
||||||
|
size="icon-sm"
|
||||||
|
className="size-auto gap-1"
|
||||||
|
onClick={clearLogs}
|
||||||
|
disabled={state === PreflightWorkerState.running}
|
||||||
|
>
|
||||||
|
<Cross2Icon className="shrink-0" height="12" />
|
||||||
|
Clear Output
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<section
|
||||||
|
ref={consoleRef}
|
||||||
|
className='h-1/2 overflow-hidden overflow-y-scroll bg-[#10151f] py-2.5 pl-[26px] pr-2.5 font-[Menlo,Monaco,"Courier_New",monospace] text-xs/[18px]'
|
||||||
|
data-cy="console-output"
|
||||||
|
>
|
||||||
|
{logs.map((log, index) => {
|
||||||
|
let type = '';
|
||||||
|
if (log instanceof Error) {
|
||||||
|
type = 'error';
|
||||||
|
log = `${log.name}: ${log.message}`;
|
||||||
|
}
|
||||||
|
if (typeof log === 'string') {
|
||||||
|
type ||= log.split(':')[0].toLowerCase();
|
||||||
|
|
||||||
|
const ComponentToUse = {
|
||||||
|
error: CrossCircledIcon,
|
||||||
|
warn: ExclamationTriangleIcon,
|
||||||
|
info: InfoCircledIcon,
|
||||||
|
}[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={clsx(
|
||||||
|
'relative',
|
||||||
|
{
|
||||||
|
error: 'text-red-500',
|
||||||
|
warn: 'text-yellow-500',
|
||||||
|
info: 'text-green-500',
|
||||||
|
}[type],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ComponentToUse && <ComponentToUse className={classes.icon} />}
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <hr key={index} className="my-2 border-dashed border-current" />;
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
<Title className="flex gap-2 p-2">
|
||||||
|
Environment Variables
|
||||||
|
<Badge className="text-xs" variant="outline">
|
||||||
|
JSON
|
||||||
|
</Badge>
|
||||||
|
</Title>
|
||||||
|
<MonacoEditor
|
||||||
|
value={envValue}
|
||||||
|
onChange={value => onEnvValueChange(value ?? '')}
|
||||||
|
onMount={handleEnvEditorDidMount}
|
||||||
|
{...monacoProps.env}
|
||||||
|
options={{
|
||||||
|
...monacoProps.env.options,
|
||||||
|
wordWrap: 'wordWrapColumn',
|
||||||
|
}}
|
||||||
|
wrapperProps={{
|
||||||
|
['data-cy']: 'env-editor',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="items-center">
|
||||||
|
<p className="me-5 flex items-center gap-2 text-sm">
|
||||||
|
<InfoCircledIcon />
|
||||||
|
Changes made to this Preflight Script will apply to all users on your team using this
|
||||||
|
target.
|
||||||
|
</p>
|
||||||
|
<Button type="button" onClick={toggle} data-cy="preflight-script-modal-cancel">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
data-cy="preflight-script-modal-submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
packages/web/app/src/lib/preflight-sandbox/json.ts
Normal file
29
packages/web/app/src/lib/preflight-sandbox/json.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export type JSONPrimitive = boolean | null | string | number;
|
||||||
|
export type JSONObject = { [key: string]: JSONValue };
|
||||||
|
export type JSONValue = JSONPrimitive | JSONValue[] | JSONObject;
|
||||||
|
|
||||||
|
function isJSONValue(value: unknown): value is JSONValue {
|
||||||
|
return (
|
||||||
|
(Array.isArray(value) && value.every(isJSONValue)) ||
|
||||||
|
isJSONObject(value) ||
|
||||||
|
isJSONPrimitive(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJSONObject(value: unknown): value is JSONObject {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!!value &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
Object.values(value).every(isJSONValue)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJSONPrimitive(value: unknown): value is JSONPrimitive {
|
||||||
|
return (
|
||||||
|
typeof value === 'boolean' ||
|
||||||
|
typeof value === 'number' ||
|
||||||
|
typeof value === 'string' ||
|
||||||
|
value === null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
import CryptoJSPackageJson from 'crypto-js/package.json';
|
||||||
|
import { ALLOWED_GLOBALS } from './allowed-globals';
|
||||||
|
import { isJSONPrimitive, JSONPrimitive } from './json';
|
||||||
|
import { WorkerEvents } from './shared-types';
|
||||||
|
|
||||||
|
export type LogMessage = string | Error;
|
||||||
|
|
||||||
|
function sendMessage(data: WorkerEvents.Outgoing.EventData) {
|
||||||
|
postMessage(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = async event => {
|
||||||
|
await execute(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener('unhandledrejection', event => {
|
||||||
|
const error = 'reason' in event ? new Error(event.reason) : event;
|
||||||
|
sendMessage({ type: WorkerEvents.Outgoing.Event.error, error });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function execute(args: {
|
||||||
|
environmentVariables: Record<string, JSONPrimitive>;
|
||||||
|
script: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { environmentVariables, script } = args;
|
||||||
|
const inWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
|
||||||
|
// Confirm the build pipeline worked and this is running inside a worker and not the main thread
|
||||||
|
if (!inWorker) {
|
||||||
|
throw new Error(
|
||||||
|
'Preflight script must always be run in web workers, this is a problem with laboratory not user input',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
for (const key in globalThis) {
|
||||||
|
allGlobalKeys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out allowed global variables and keys that will cause problems
|
||||||
|
const blockedGlobals = allGlobalKeys.filter(
|
||||||
|
key =>
|
||||||
|
// When testing in the main thread this exists on window and is not a valid argument name.
|
||||||
|
// because global is blocked, even if this was in the worker it's still wouldn't be available because it's not a valid variable name
|
||||||
|
!key.includes('-') &&
|
||||||
|
!ALLOWED_GLOBALS.has(key) &&
|
||||||
|
// window has references as indexes on the globalThis such as `globalThis[0]`, numbers are not valid arguments, so we need to filter these out
|
||||||
|
Number.isNaN(Number(key)) &&
|
||||||
|
// @ is not a valid argument name beginning character, so we don't need to block it and including it will cause a syntax error
|
||||||
|
// only example currently is @wry/context which is a dep of @apollo/client and adds @wry/context:Slot
|
||||||
|
key.charAt(0) !== '@',
|
||||||
|
);
|
||||||
|
// restrict window variable
|
||||||
|
blockedGlobals.push('window');
|
||||||
|
|
||||||
|
const log =
|
||||||
|
(level: 'log' | 'warn' | 'error' | 'info') =>
|
||||||
|
(...args: unknown[]) => {
|
||||||
|
console[level](...args);
|
||||||
|
let message = `${level.charAt(0).toUpperCase()}${level.slice(1)}: ${args.map(String).join(' ')}`;
|
||||||
|
message += appendLineAndColumn(new Error(), {
|
||||||
|
columnOffset: 'console.'.length,
|
||||||
|
});
|
||||||
|
// The messages should be streamed to the main thread as they occur not gathered and send to
|
||||||
|
// the main thread at the end of the execution of the preflight script
|
||||||
|
postMessage({ type: 'log', message });
|
||||||
|
};
|
||||||
|
|
||||||
|
function getValidEnvVariable(value: unknown) {
|
||||||
|
if (isJSONPrimitive(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
consoleApi.warn(
|
||||||
|
'You tried to set a non primitive type in env variables, only string, boolean, number and null are allowed in env variables. The value has been filtered out.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const consoleApi = Object.freeze({
|
||||||
|
log: log('log'),
|
||||||
|
info: log('info'),
|
||||||
|
warn: log('warn'),
|
||||||
|
error: log('error'),
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasLoggedCryptoJSVersion = false;
|
||||||
|
|
||||||
|
const labApi = Object.freeze({
|
||||||
|
get CryptoJS() {
|
||||||
|
if (!hasLoggedCryptoJSVersion) {
|
||||||
|
hasLoggedCryptoJSVersion = true;
|
||||||
|
consoleApi.info(`Using crypto-js version: ${CryptoJSPackageJson.version}`);
|
||||||
|
}
|
||||||
|
return CryptoJS;
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
get(key: string) {
|
||||||
|
return Object.freeze(workingEnvironmentVariables[key]);
|
||||||
|
},
|
||||||
|
set(key: string, value: unknown) {
|
||||||
|
const validValue = getValidEnvVariable(value);
|
||||||
|
if (validValue === undefined) {
|
||||||
|
delete workingEnvironmentVariables[key];
|
||||||
|
} else {
|
||||||
|
workingEnvironmentVariables[key] = validValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wrap the users script in an async IIFE to allow the use of top level await
|
||||||
|
const rawJs = `return(async()=>{'use strict';
|
||||||
|
${script}})()`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Function(
|
||||||
|
'lab',
|
||||||
|
'console',
|
||||||
|
// spreading all the variables we want to block creates an argument that shadows their names, any attempt to access them will result in `undefined`
|
||||||
|
...blockedGlobals,
|
||||||
|
rawJs,
|
||||||
|
// Bind the function to a null constructor object to prevent `this` leaking scope in
|
||||||
|
).bind(
|
||||||
|
// When `this` is `undefined` or `null`, we get [object DedicatedWorkerGlobalScope] in console output
|
||||||
|
// instead we set as string `'undefined'` so in console, we'll see undefined as well
|
||||||
|
'undefined',
|
||||||
|
)(labApi, consoleApi);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
error.message += appendLineAndColumn(error);
|
||||||
|
}
|
||||||
|
sendMessage({ type: WorkerEvents.Outgoing.Event.error, error: error as Error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendMessage({
|
||||||
|
type: WorkerEvents.Outgoing.Event.result,
|
||||||
|
environmentVariables: workingEnvironmentVariables,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLineAndColumn(error: Error, { columnOffset = 0 } = {}): string {
|
||||||
|
const regex = /<anonymous>:(?<line>\d+):(?<column>\d+)/; // Regex to match the line and column numbers
|
||||||
|
|
||||||
|
const { line, column } = error.stack?.match(regex)?.groups || {};
|
||||||
|
return ` (Line: ${Number(line) - 3}, Column: ${Number(column) - columnOffset})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage({ type: WorkerEvents.Outgoing.Event.ready });
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import PreflightWorker from './preflight-script-worker?worker&inline';
|
||||||
|
import { IFrameEvents, WorkerEvents } from './shared-types';
|
||||||
|
|
||||||
|
function postMessage(data: IFrameEvents.Outgoing.EventData) {
|
||||||
|
window.parent.postMessage(data, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFLIGHT_TIMEOUT = 30_000;
|
||||||
|
|
||||||
|
const abortSignals = new Map<string, AbortController>();
|
||||||
|
|
||||||
|
window.addEventListener('message', (e: IFrameEvents.Incoming.MessageEvent) => {
|
||||||
|
console.log('received event', e.data);
|
||||||
|
|
||||||
|
if (e.data.type === IFrameEvents.Incoming.Event.run) {
|
||||||
|
handleRunEvent(e.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.data.type === IFrameEvents.Incoming.Event.abort) {
|
||||||
|
abortSignals.get(e.data.id)?.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
type: IFrameEvents.Outgoing.Event.ready,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleRunEvent(data: IFrameEvents.Incoming.RunEventData) {
|
||||||
|
let worker: Worker;
|
||||||
|
|
||||||
|
function terminate() {
|
||||||
|
if (worker) {
|
||||||
|
worker.terminate();
|
||||||
|
}
|
||||||
|
abortSignals.delete(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
controller.signal.onabort = terminate;
|
||||||
|
abortSignals.set(data.id, controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
worker = new PreflightWorker();
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
postMessage({
|
||||||
|
type: IFrameEvents.Outgoing.Event.error,
|
||||||
|
runId: data.id,
|
||||||
|
error: new Error(
|
||||||
|
`Preflight script execution timed out after ${PREFLIGHT_TIMEOUT / 1000} seconds`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
terminate();
|
||||||
|
}, PREFLIGHT_TIMEOUT);
|
||||||
|
|
||||||
|
worker.addEventListener(
|
||||||
|
'message',
|
||||||
|
function eventListener(ev: WorkerEvents.Outgoing.MessageEvent) {
|
||||||
|
console.log('received event from worker', ev.data);
|
||||||
|
if (ev.data.type === WorkerEvents.Outgoing.Event.ready) {
|
||||||
|
worker.postMessage({
|
||||||
|
script: data.script,
|
||||||
|
environmentVariables: data.environmentVariables,
|
||||||
|
} satisfies WorkerEvents.Incoming.MessageData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.data.type === WorkerEvents.Outgoing.Event.result) {
|
||||||
|
postMessage({
|
||||||
|
type: IFrameEvents.Outgoing.Event.result,
|
||||||
|
runId: data.id,
|
||||||
|
environmentVariables: ev.data.environmentVariables,
|
||||||
|
});
|
||||||
|
clearTimeout(timeout);
|
||||||
|
terminate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.data.type === WorkerEvents.Outgoing.Event.log) {
|
||||||
|
postMessage({
|
||||||
|
type: IFrameEvents.Outgoing.Event.log,
|
||||||
|
runId: data.id,
|
||||||
|
log: ev.data.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.data.type === WorkerEvents.Outgoing.Event.error) {
|
||||||
|
postMessage({
|
||||||
|
type: IFrameEvents.Outgoing.Event.error,
|
||||||
|
runId: data.id,
|
||||||
|
error: ev.data.error,
|
||||||
|
});
|
||||||
|
clearTimeout(timeout);
|
||||||
|
terminate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
type: IFrameEvents.Outgoing.Event.start,
|
||||||
|
runId: data.id,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
postMessage({
|
||||||
|
type: IFrameEvents.Outgoing.Event.error,
|
||||||
|
runId: data.id,
|
||||||
|
error: error as Error,
|
||||||
|
});
|
||||||
|
terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
packages/web/app/src/lib/preflight-sandbox/shared-types.ts
Normal file
101
packages/web/app/src/lib/preflight-sandbox/shared-types.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
|
||||||
|
type _MessageEvent<T> = MessageEvent<T>;
|
||||||
|
|
||||||
|
export namespace IFrameEvents {
|
||||||
|
export namespace Outgoing {
|
||||||
|
export const enum Event {
|
||||||
|
ready = 'ready',
|
||||||
|
start = 'start',
|
||||||
|
log = 'log',
|
||||||
|
result = 'result',
|
||||||
|
error = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReadyEventData = {
|
||||||
|
type: Event.ready;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StartEventData = {
|
||||||
|
type: Event.start;
|
||||||
|
runId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogEventData = {
|
||||||
|
type: Event.log;
|
||||||
|
runId: string;
|
||||||
|
log: string | Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResultEventData = {
|
||||||
|
type: Event.result;
|
||||||
|
runId: string;
|
||||||
|
environmentVariables: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ErrorEventData = {
|
||||||
|
type: Event.error;
|
||||||
|
runId: string;
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventData =
|
||||||
|
| ReadyEventData
|
||||||
|
| StartEventData
|
||||||
|
| LogEventData
|
||||||
|
| ResultEventData
|
||||||
|
| ErrorEventData;
|
||||||
|
|
||||||
|
export type MessageEvent = _MessageEvent<EventData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Incoming {
|
||||||
|
export const enum Event {
|
||||||
|
run = 'run',
|
||||||
|
abort = 'abort',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RunEventData = {
|
||||||
|
type: Event.run;
|
||||||
|
id: string;
|
||||||
|
script: string;
|
||||||
|
environmentVariables: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AbortEventData = {
|
||||||
|
type: Event.abort;
|
||||||
|
id: string;
|
||||||
|
script: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EventData = RunEventData | AbortEventData;
|
||||||
|
|
||||||
|
export type MessageEvent = _MessageEvent<EventData>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace WorkerEvents {
|
||||||
|
export namespace Outgoing {
|
||||||
|
export const enum Event {
|
||||||
|
ready = 'ready',
|
||||||
|
log = 'log',
|
||||||
|
result = 'result',
|
||||||
|
error = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogEventData = { type: Event.log; message: string };
|
||||||
|
type ErrorEventData = { type: Event.error; error: Error };
|
||||||
|
type ResultEventData = { type: Event.result; environmentVariables: Record<string, unknown> };
|
||||||
|
type ReadyEventData = { type: Event.ready };
|
||||||
|
|
||||||
|
export type EventData = ResultEventData | LogEventData | ErrorEventData | ReadyEventData;
|
||||||
|
export type MessageEvent = _MessageEvent<EventData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Incoming {
|
||||||
|
export type MessageData = {
|
||||||
|
script: string;
|
||||||
|
environmentVariables: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,11 @@ import {
|
||||||
import { useSyncOperationState } from '@/lib/hooks/laboratory/use-sync-operation-state';
|
import { useSyncOperationState } from '@/lib/hooks/laboratory/use-sync-operation-state';
|
||||||
import { useOperationFromQueryString } from '@/lib/hooks/laboratory/useOperationFromQueryString';
|
import { useOperationFromQueryString } from '@/lib/hooks/laboratory/useOperationFromQueryString';
|
||||||
import { useResetState } from '@/lib/hooks/use-reset-state';
|
import { useResetState } from '@/lib/hooks/use-reset-state';
|
||||||
|
import {
|
||||||
|
preflightScriptPlugin,
|
||||||
|
PreflightScriptProvider,
|
||||||
|
usePreflightScript,
|
||||||
|
} from '@/lib/preflight-sandbox/graphiql-plugin';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { explorerPlugin } from '@graphiql/plugin-explorer';
|
import { explorerPlugin } from '@graphiql/plugin-explorer';
|
||||||
import {
|
import {
|
||||||
|
|
@ -52,7 +57,7 @@ import { useRedirect } from '@/lib/access/common';
|
||||||
const explorer = explorerPlugin();
|
const explorer = explorerPlugin();
|
||||||
|
|
||||||
// Declare outside components, otherwise while clicking on field in explorer operationCollectionsPlugin will be open
|
// Declare outside components, otherwise while clicking on field in explorer operationCollectionsPlugin will be open
|
||||||
const plugins = [explorer, operationCollectionsPlugin];
|
const plugins = [explorer, operationCollectionsPlugin, preflightScriptPlugin];
|
||||||
|
|
||||||
function Share(): ReactElement | null {
|
function Share(): ReactElement | null {
|
||||||
const label = 'Share query';
|
const label = 'Share query';
|
||||||
|
|
@ -243,11 +248,30 @@ function Save(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function substituteVariablesInHeader(
|
||||||
|
headers: Record<string, string>,
|
||||||
|
environmentVariables: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
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(/{{(?<keyName>.*?)}}/g, (originalString, envKey) => {
|
||||||
|
return Object.hasOwn(environmentVariables, envKey)
|
||||||
|
? (environmentVariables[envKey] as string)
|
||||||
|
: originalString;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [key, value];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function LaboratoryPageContent(props: {
|
function LaboratoryPageContent(props: {
|
||||||
organizationSlug: string;
|
organizationSlug: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
targetSlug: string;
|
targetSlug: string;
|
||||||
selectedOperationId: string | undefined;
|
selectedOperationId?: string;
|
||||||
}) {
|
}) {
|
||||||
const [query] = useQuery({
|
const [query] = useQuery({
|
||||||
query: TargetLaboratoryPageQuery,
|
query: TargetLaboratoryPageQuery,
|
||||||
|
|
@ -280,21 +304,58 @@ function LaboratoryPageContent(props: {
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockEndpoint = `${location.origin}/api/lab/${props.organizationSlug}/${props.projectSlug}/${props.targetSlug}`;
|
const mockEndpoint = `${location.origin}/api/lab/${props.organizationSlug}/${props.projectSlug}/${props.targetSlug}`;
|
||||||
|
const target = query.data?.target;
|
||||||
|
|
||||||
|
const preflightScript = usePreflightScript({ target: target ?? null });
|
||||||
|
|
||||||
const fetcher = useMemo<Fetcher>(() => {
|
const fetcher = useMemo<Fetcher>(() => {
|
||||||
return async (params, opts) => {
|
return async (params, opts) => {
|
||||||
|
let headers = opts?.headers;
|
||||||
const url =
|
const url =
|
||||||
(actualSelectedApiEndpoint === 'linkedApi'
|
(actualSelectedApiEndpoint === 'linkedApi' ? target?.graphqlEndpointUrl : undefined) ??
|
||||||
? query.data?.target?.graphqlEndpointUrl
|
mockEndpoint;
|
||||||
: undefined) ?? mockEndpoint;
|
|
||||||
|
|
||||||
const _fetcher = createGraphiQLFetcher({ url, fetch });
|
return new Repeater(async (push, stop) => {
|
||||||
|
let hasFinishedPreflightScript = false;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
stop.then(() => {
|
||||||
|
if (!hasFinishedPreflightScript) {
|
||||||
|
preflightScript.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const result = await preflightScript.execute();
|
||||||
|
if (result && headers) {
|
||||||
|
headers = substituteVariablesInHeader(headers, result);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error === false) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const formatError = JSON.stringify(
|
||||||
|
{
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
const error = new Error(`Error during preflight script execution:\n\n${formatError}`);
|
||||||
|
// We only want to expose the error message, not the whole stack trace.
|
||||||
|
delete error.stack;
|
||||||
|
stop(error);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
hasFinishedPreflightScript = true;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await _fetcher(params, opts);
|
const graphiqlFetcher = createGraphiQLFetcher({ url, fetch });
|
||||||
|
const result = await graphiqlFetcher(params, {
|
||||||
|
...opts,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
// We only want to expose the error message, not the whole stack trace.
|
if (isAsyncIterable(result)) {
|
||||||
if (isAsyncIterable(result)) {
|
|
||||||
return new Repeater(async (push, stop) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
stop.then(
|
stop.then(
|
||||||
() => 'return' in result && result.return instanceof Function && result.return(),
|
() => 'return' in result && result.return instanceof Function && result.return(),
|
||||||
|
|
@ -306,15 +367,22 @@ function LaboratoryPageContent(props: {
|
||||||
stop();
|
stop();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = new Error(err instanceof Error ? err.message : 'Unexpected error.');
|
const error = new Error(err instanceof Error ? err.message : 'Unexpected error.');
|
||||||
|
// We only want to expose the error message, not the whole stack trace.
|
||||||
delete error.stack;
|
delete error.stack;
|
||||||
stop(error);
|
stop(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, [query.data?.target?.graphqlEndpointUrl, actualSelectedApiEndpoint]);
|
}, [
|
||||||
|
target?.graphqlEndpointUrl,
|
||||||
|
actualSelectedApiEndpoint,
|
||||||
|
preflightScript.execute,
|
||||||
|
preflightScript.isPreflightScriptEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const FullScreenIcon = isFullScreen ? ExitFullScreenIcon : EnterFullScreenIcon;
|
const FullScreenIcon = isFullScreen ? ExitFullScreenIcon : EnterFullScreenIcon;
|
||||||
|
|
||||||
|
|
@ -335,8 +403,6 @@ function LaboratoryPageContent(props: {
|
||||||
[userOperations],
|
[userOperations],
|
||||||
);
|
);
|
||||||
|
|
||||||
const target = query.data?.target;
|
|
||||||
|
|
||||||
useRedirect({
|
useRedirect({
|
||||||
canAccess: target?.viewerCanViewLaboratory === true,
|
canAccess: target?.viewerCanViewLaboratory === true,
|
||||||
redirectTo: router => {
|
redirectTo: router => {
|
||||||
|
|
@ -368,6 +434,7 @@ function LaboratoryPageContent(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{preflightScript.iframeElement}
|
||||||
<div className="flex py-6">
|
<div className="flex py-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Title>Laboratory</Title>
|
<Title>Laboratory</Title>
|
||||||
|
|
@ -448,7 +515,9 @@ function LaboratoryPageContent(props: {
|
||||||
.graphiql-dialog a {
|
.graphiql-dialog a {
|
||||||
--color-primary: 40, 89%, 60% !important;
|
--color-primary: 40, 89%, 60% !important;
|
||||||
}
|
}
|
||||||
|
.graphiql-container {
|
||||||
|
overflow: unset; /* remove default overflow */
|
||||||
|
}
|
||||||
.graphiql-container,
|
.graphiql-container,
|
||||||
.graphiql-dialog,
|
.graphiql-dialog,
|
||||||
.CodeMirror-info {
|
.CodeMirror-info {
|
||||||
|
|
@ -464,49 +533,49 @@ function LaboratoryPageContent(props: {
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
{!query.fetching && !query.stale && (
|
{!query.fetching && !query.stale && (
|
||||||
<GraphiQL
|
<PreflightScriptProvider value={preflightScript}>
|
||||||
fetcher={fetcher}
|
<GraphiQL
|
||||||
showPersistHeadersSettings={false}
|
fetcher={fetcher}
|
||||||
shouldPersistHeaders={false}
|
shouldPersistHeaders
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
visiblePlugin={operationCollectionsPlugin}
|
visiblePlugin={operationCollectionsPlugin}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
forcedTheme="dark"
|
forcedTheme="dark"
|
||||||
className={isFullScreen ? 'fixed inset-0 bg-[#030711]' : ''}
|
className={isFullScreen ? 'fixed inset-0 bg-[#030711]' : ''}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
readOnly={!!props.selectedOperationId && target?.viewerCanModifyLaboratory === false}
|
readOnly={!!props.selectedOperationId && target?.viewerCanModifyLaboratory === false}
|
||||||
>
|
>
|
||||||
<GraphiQL.Logo>
|
<GraphiQL.Logo>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsFullScreen(prev => !prev)}
|
onClick={() => setIsFullScreen(prev => !prev)}
|
||||||
variant="orangeLink"
|
variant="orangeLink"
|
||||||
className="gap-2 whitespace-nowrap"
|
className="gap-2 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<FullScreenIcon className="size-4" />
|
<FullScreenIcon className="size-4" />
|
||||||
{isFullScreen ? 'Exit' : 'Enter'} Full Screen
|
{isFullScreen ? 'Exit' : 'Enter'} Full Screen
|
||||||
</Button>
|
</Button>
|
||||||
</GraphiQL.Logo>
|
</GraphiQL.Logo>
|
||||||
<GraphiQL.Toolbar>
|
<GraphiQL.Toolbar>
|
||||||
{({ prettify }) => (
|
{({ prettify }) => (
|
||||||
<>
|
<>
|
||||||
{query.data?.target?.viewerCanModifyLaboratory && (
|
{query.data?.target?.viewerCanModifyLaboratory && (
|
||||||
<Save
|
<Save
|
||||||
organizationSlug={props.organizationSlug}
|
organizationSlug={props.organizationSlug}
|
||||||
projectSlug={props.projectSlug}
|
projectSlug={props.projectSlug}
|
||||||
targetSlug={props.targetSlug}
|
targetSlug={props.targetSlug}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Share />
|
<Share />
|
||||||
{/* if people have no modify access they should still be able to format their own queries. */}
|
{/* if people have no modify access they should still be able to format their own queries. */}
|
||||||
{(query.data?.target?.viewerCanModifyLaboratory === true ||
|
{(query.data?.target?.viewerCanModifyLaboratory === true ||
|
||||||
!props.selectedOperationId) &&
|
!props.selectedOperationId) &&
|
||||||
prettify}
|
prettify}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</GraphiQL.Toolbar>
|
</GraphiQL.Toolbar>
|
||||||
</GraphiQL>
|
</GraphiQL>
|
||||||
|
</PreflightScriptProvider>
|
||||||
)}
|
)}
|
||||||
<ConnectLabModal
|
<ConnectLabModal
|
||||||
endpoint={mockEndpoint}
|
endpoint={mockEndpoint}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ const server = Fastify({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const preflightWorkerEmbed = {
|
||||||
|
path: '/__preflight-embed',
|
||||||
|
htmlFile: 'preflight-worker-embed.html',
|
||||||
|
};
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
/**
|
/**
|
||||||
* Why is this necessary?
|
* Why is this necessary?
|
||||||
|
|
@ -35,6 +40,17 @@ async function main() {
|
||||||
server.log.info('Running in development mode');
|
server.log.info('Running in development mode');
|
||||||
// If in development mode, use Vite to serve the frontend and enable hot module reloading.
|
// If in development mode, use Vite to serve the frontend and enable hot module reloading.
|
||||||
const { default: FastifyVite } = await import('@fastify/vite');
|
const { default: FastifyVite } = await import('@fastify/vite');
|
||||||
|
|
||||||
|
// This and a patch of @fastify/vite is necessary to serve the preflight worker embed html file.
|
||||||
|
// We need to know if the request is for the preflight worker embed or not to determine which html file to serve.
|
||||||
|
server.decorateRequest('viteHtmlFile', {
|
||||||
|
getter() {
|
||||||
|
return this.url.startsWith(preflightWorkerEmbed.path)
|
||||||
|
? preflightWorkerEmbed.htmlFile
|
||||||
|
: 'index.html';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await server.register(FastifyVite, {
|
await server.register(FastifyVite, {
|
||||||
// The root directory of @hive/app (where the package.json is located)
|
// The root directory of @hive/app (where the package.json is located)
|
||||||
// /
|
// /
|
||||||
|
|
@ -85,6 +101,18 @@ async function main() {
|
||||||
connectGithub(server);
|
connectGithub(server);
|
||||||
connectLab(server);
|
connectLab(server);
|
||||||
|
|
||||||
|
server.get(preflightWorkerEmbed.path, (_req, reply) => {
|
||||||
|
if (isDev) {
|
||||||
|
// If in development mode, return the Vite preflight-worker-embed.html.
|
||||||
|
return reply.html();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If in production mode, return the static html file.
|
||||||
|
return reply.sendFile(preflightWorkerEmbed.htmlFile, {
|
||||||
|
cacheControl: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
server.get('*', (_req, reply) => {
|
server.get('*', (_req, reply) => {
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// If in development mode, return the Vite index.html.
|
// If in development mode, return the Vite index.html.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "../../../tsconfig.json",
|
"extends": "../../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { resolve } from 'node:path';
|
||||||
import type { UserConfig } from 'vite';
|
import type { UserConfig } from 'vite';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
@ -7,4 +8,15 @@ const __dirname = new URL('.', import.meta.url).pathname;
|
||||||
export default {
|
export default {
|
||||||
root: __dirname,
|
root: __dirname,
|
||||||
plugins: [tsconfigPaths(), react()],
|
plugins: [tsconfigPaths(), react()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, 'index.html'),
|
||||||
|
['preflight-worker-embed']: resolve(__dirname, 'preflight-worker-embed.html'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} satisfies UserConfig;
|
} satisfies UserConfig;
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 397 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 309 KiB |
|
|
@ -1,20 +1,22 @@
|
||||||
import NextImage from 'next/image'
|
---
|
||||||
|
title: Overview
|
||||||
|
---
|
||||||
|
|
||||||
import { Callout } from '@theguild/components'
|
import { Callout } from '@theguild/components'
|
||||||
import labFormImage from '../../../../public/docs/pages/features/lab-form.png'
|
import { Screenshot } from '../../../../components/screenshot'
|
||||||
import labImage from '../../../../public/docs/pages/features/lab.png'
|
|
||||||
|
|
||||||
# Laboratory
|
# Laboratory
|
||||||
|
|
||||||
Under your target page, you'll find the **Laboratory** page. The Laboratory allows you to explore
|
Under your target page, you'll find the **Laboratory** page. The Laboratory allows you to explore
|
||||||
your GraphQL schema and run queries against a mocked version of your GraphQL service.
|
your GraphQL schema and run queries against a mocked version of your GraphQL service.
|
||||||
|
|
||||||
## Explore your GraphQL schema
|
## Explore your GraphQL Schema
|
||||||
|
|
||||||
You can use the full power of [GraphiQL](https://github.com/graphql/graphiql) directly within Hive:
|
You can use the full power of [GraphiQL](https://github.com/graphql/graphiql) directly within Hive:
|
||||||
compose your GraphQL operations, explore with different field and variations, and access your
|
compose your GraphQL operations, explore with different field and variations, and access your
|
||||||
GraphQL schema full documentation.
|
GraphQL schema full documentation.
|
||||||
|
|
||||||
<NextImage alt="Lab" src={labImage} className="mt-6 max-w-3xl rounded-lg drop-shadow-md" />
|
<Screenshot></Screenshot>
|
||||||
|
|
||||||
## Link a Laboratory Endpoint
|
## Link a Laboratory Endpoint
|
||||||
|
|
||||||
|
|
@ -49,7 +51,7 @@ To get started with using the Laboratory mock schema externally, create a
|
||||||
Now, click on the **Use Schema Externally** button on the Laboratory page, and follow the
|
Now, click on the **Use Schema Externally** button on the Laboratory page, and follow the
|
||||||
instructions on the form:
|
instructions on the form:
|
||||||
|
|
||||||
<NextImage alt="Lab Form" src={labFormImage} className="mt-6 max-w-xl rounded-lg drop-shadow-md" />
|
<Screenshot></Screenshot>
|
||||||
|
|
||||||
To test access to your setup, try running a `curl` command to run a simple GraphQL query against
|
To test access to your setup, try running a `curl` command to run a simple GraphQL query against
|
||||||
your mocked schema:
|
your mocked schema:
|
||||||
|
|
@ -60,7 +62,7 @@ curl -X POST -H "X-Hive-Key: HIVE_TOKEN_HERE" -H "Content-Type: application/json
|
||||||
--data-raw '{"query": "{ __typename }"}'
|
--data-raw '{"query": "{ __typename }"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### With GraphQL-Code-Generator
|
### With GraphQL Code Generator
|
||||||
|
|
||||||
<Callout>
|
<Callout>
|
||||||
We recommend using the CDN for consuming the GraphQL schema in your project. [See GraphQL Code
|
We recommend using the CDN for consuming the GraphQL schema in your project. [See GraphQL Code
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 476 KiB |
|
|
@ -0,0 +1,99 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
Useful for handling authentication flows like OAuth, where you may need to refresh an access token
|
||||||
|
---
|
||||||
|
|
||||||
|
# Preflight Scripts
|
||||||
|
|
||||||
|
import { Callout } from '@theguild/components'
|
||||||
|
import { Screenshot } from '../../../../components/screenshot'
|
||||||
|
|
||||||
|
export const figcaptionClass = 'text-center text-sm mt-2'
|
||||||
|
|
||||||
|
These scripts allow you to automatically run custom authentication processes before executing your
|
||||||
|
GraphQL operations. They're especially useful for handling authentication flows like OAuth, where
|
||||||
|
you may need to refresh an access token. Let's explore how it works.
|
||||||
|
|
||||||
|
## Configuring Preflight Script
|
||||||
|
|
||||||
|
To create a script click on the command line icon (right after Operation Collections plugin icon) in
|
||||||
|
GraphiQL sidebar section.
|
||||||
|
|
||||||
|
<figure className="mt-6">
|
||||||
|
<Screenshot></Screenshot>
|
||||||
|
{/* prettier-ignore */}
|
||||||
|
<figcaption className={figcaptionClass}>The preflight script is accessible by clicking on the Command line icon in the GraphiQL sidebar</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
You will see Script editor (JavaScript language) which is read-only and present for a quick view of
|
||||||
|
your saved script and Environment variables editor (JSON language) which is persistent in
|
||||||
|
localStorage.
|
||||||
|
|
||||||
|
<figure className="mt-6">
|
||||||
|
<Screenshot></Screenshot>
|
||||||
|
<figcaption className={figcaptionClass}>Preflight script plugin view</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Editing Preflight Script
|
||||||
|
|
||||||
|
Clicking on the `Edit` button will open Modal where you can edit, test and save your script in
|
||||||
|
database.
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
**Note**: Your script will stored as plain text in our database, don't put any secrets there, use
|
||||||
|
Environment variables editor for it! The preflight script is accessible to all members of your
|
||||||
|
organization, but only users with access to target Settings can edit the script code.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
You can use any JavaScript syntax (including top-level `await`) in the Script editor. Getting and
|
||||||
|
Setting environment variables is done by accessing the `environment` property on the `lab` global
|
||||||
|
variable.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// get myKey variable from the Environment variables editor
|
||||||
|
lab.environment.get('myKey')
|
||||||
|
// set myKey variable to the Environment variables editor (persistent in localStorage)
|
||||||
|
lab.environment.set('myKey', myValue)
|
||||||
|
```
|
||||||
|
|
||||||
|
<figure className="mt-6">
|
||||||
|
<Screenshot></Screenshot>
|
||||||
|
<figcaption className={figcaptionClass}>Demo how to get and set environment variables</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## CryptoJS
|
||||||
|
|
||||||
|
Additionally, you can access [the CryptoJS library](https://github.com/brix/crypto-js) by accessing
|
||||||
|
the `CryptoJS` property on the `lab` global variable.
|
||||||
|
|
||||||
|
<figure className="mt-6">
|
||||||
|
<Screenshot></Screenshot>
|
||||||
|
<figcaption className={figcaptionClass}>CryptoJS</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Global Variables and Errors
|
||||||
|
|
||||||
|
Access to global variables such as `this`, `window` or `globalThis` is restricted. Errors thrown by
|
||||||
|
the script will be displayed in Console Output.
|
||||||
|
|
||||||
|
<figure className="mt-6">
|
||||||
|
<Screenshot></Screenshot>
|
||||||
|
<figcaption className={figcaptionClass}>Demo restricted access to global variables</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Using Environment Variables
|
||||||
|
|
||||||
|
To use your environment variables in GraphiQL headers editor wraps environment keys with
|
||||||
|
double-curly braces, e.g.:
|
||||||
|
|
||||||
|
```json filename="Headers" /{{myEnvVar}}/
|
||||||
|
{
|
||||||
|
"Authorization": "Bearer {{myEnvVar}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<figure className="mt-6">
|
||||||
|
<Screenshot></Screenshot>
|
||||||
|
{/* prettier-ignore */}
|
||||||
|
<figcaption className={figcaptionClass}>Replace syntax is done via double open/closed curly braces, e.g. `{{ myEnvVar }}`</figcaption>
|
||||||
|
</figure>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
title: Preflight Scripts in the Laboratory
|
||||||
|
description:
|
||||||
|
Populate headers with environment variables and automate authentication flows for GraphQL
|
||||||
|
requests.
|
||||||
|
date: 2024-12-27
|
||||||
|
authors: [dimitri]
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout } from '@theguild/components'
|
||||||
|
|
||||||
|
export const figcaptionClass = 'text-center text-sm mt-2'
|
||||||
|
|
||||||
|
We've added Preflight Scripts to Laboratory! These scripts allow you to automatically run custom
|
||||||
|
authentication processes before executing your GraphQL operations. They're especially useful for
|
||||||
|
handling authentication flows like OAuth, where you may need to refresh an access token.
|
||||||
|
|
||||||
|
<figure className="mt-6">
|
||||||
|
<></>
|
||||||
|
<figcaption className={figcaptionClass}>Demo of Preflight Scripts</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Check out [the documentation on Preflight Scripts](/docs/dashboard/laboratory/preflight-scripts) for
|
||||||
|
information on how to configure, edit, and use them.
|
||||||
17
patches/@fastify__vite.patch
Normal file
17
patches/@fastify__vite.patch
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
diff --git a/mode/development.js b/mode/development.js
|
||||||
|
index af9de9d75a3689cd4f4b5d2876f2e38bd2674ae4..94ecb29a8e0d2615b1ecd0114dba7f3979dc2b11 100644
|
||||||
|
--- a/mode/development.js
|
||||||
|
+++ b/mode/development.js
|
||||||
|
@@ -79,7 +79,11 @@ async function setup(config) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- const indexHtmlPath = join(config.vite.root, 'index.html')
|
||||||
|
+
|
||||||
|
+ // Request is decorated with viteHtmlFile in: packages/web/app/src/server/index.ts
|
||||||
|
+ // It is used to render more than one html file
|
||||||
|
+ const htmlFileName = req.viteHtmlFile ?? 'index.html';
|
||||||
|
+ const indexHtmlPath = join(config.vite.root,htmlFileName)
|
||||||
|
const indexHtml = await read(indexHtmlPath, 'utf8')
|
||||||
|
const transformedHtml = await this.devServer.transformIndexHtml(
|
||||||
|
req.url,
|
||||||
423
patches/@graphiql__react.patch
Normal file
423
patches/@graphiql__react.patch
Normal file
|
|
@ -0,0 +1,423 @@
|
||||||
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
|
index 8ca339a2ba2031f0c1e22f1d099fa9a571492107..1cf3e8c620dc2c3ad4cfc42e2feeb4ca4682163c 100644
|
||||||
|
--- a/dist/index.mjs
|
||||||
|
+++ b/dist/index.mjs
|
||||||
|
@@ -1,6 +1,6 @@
|
||||||
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
||||||
|
import * as React from "react";
|
||||||
|
-import { createContext, useContext, useRef, useState, useEffect, forwardRef, useCallback, useMemo, useLayoutEffect } from "react";
|
||||||
|
+import { createContext, useContext, useRef, useState, useEffect, forwardRef, useMemo, useCallback, useLayoutEffect } from "react";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
import { print, astFromValue, isSchema, buildClientSchema, validateSchema, getIntrospectionQuery, isNamedType, isObjectType, isInputObjectType, isScalarType, isEnumType, isInterfaceType, isUnionType, isNonNullType, isListType, isAbstractType, isType, parse, visit } from "graphql";
|
||||||
|
import { StorageAPI, HistoryStore, formatResult, isObservable, formatError, isAsyncIterable, fetcherReturnToPromise, isPromise, mergeAst, fillLeafs, getSelectedOperationName } from "@graphiql/toolkit";
|
||||||
|
@@ -40,7 +40,7 @@ function createContextHook(context) {
|
||||||
|
const StorageContext = createNullableContext("StorageContext");
|
||||||
|
function StorageContextProvider(props) {
|
||||||
|
const isInitialRender = useRef(true);
|
||||||
|
- const [storage, setStorage] = useState(new StorageAPI(props.storage));
|
||||||
|
+ const [storage, setStorage] = useState(() => new StorageAPI(props.storage));
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialRender.current) {
|
||||||
|
isInitialRender.current = false;
|
||||||
|
@@ -465,68 +465,42 @@ const Tooltip = Object.assign(TooltipRoot, {
|
||||||
|
Provider: T.Provider
|
||||||
|
});
|
||||||
|
const HistoryContext = createNullableContext("HistoryContext");
|
||||||
|
-function HistoryContextProvider(props) {
|
||||||
|
- var _a;
|
||||||
|
+function HistoryContextProvider({
|
||||||
|
+ maxHistoryLength = DEFAULT_HISTORY_LENGTH,
|
||||||
|
+ children
|
||||||
|
+}) {
|
||||||
|
const storage = useStorageContext();
|
||||||
|
- const historyStore = useRef(
|
||||||
|
- new HistoryStore(
|
||||||
|
+ const [historyStore] = useState(
|
||||||
|
+ () => (
|
||||||
|
// Fall back to a noop storage when the StorageContext is empty
|
||||||
|
- storage || new StorageAPI(null),
|
||||||
|
- props.maxHistoryLength || DEFAULT_HISTORY_LENGTH
|
||||||
|
+ new HistoryStore(storage || new StorageAPI(null), maxHistoryLength)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
- const [items, setItems] = useState(((_a = historyStore.current) == null ? void 0 : _a.queries) || []);
|
||||||
|
- const addToHistory = useCallback(
|
||||||
|
- (operation) => {
|
||||||
|
- var _a2;
|
||||||
|
- (_a2 = historyStore.current) == null ? void 0 : _a2.updateHistory(operation);
|
||||||
|
- setItems(historyStore.current.queries);
|
||||||
|
- },
|
||||||
|
- []
|
||||||
|
- );
|
||||||
|
- const editLabel = useCallback(
|
||||||
|
- (operation, index) => {
|
||||||
|
- historyStore.current.editLabel(operation, index);
|
||||||
|
- setItems(historyStore.current.queries);
|
||||||
|
- },
|
||||||
|
- []
|
||||||
|
- );
|
||||||
|
- const toggleFavorite = useCallback(
|
||||||
|
- (operation) => {
|
||||||
|
- historyStore.current.toggleFavorite(operation);
|
||||||
|
- setItems(historyStore.current.queries);
|
||||||
|
- },
|
||||||
|
- []
|
||||||
|
- );
|
||||||
|
- const setActive = useCallback(
|
||||||
|
- (item) => {
|
||||||
|
- return item;
|
||||||
|
- },
|
||||||
|
- []
|
||||||
|
- );
|
||||||
|
- const deleteFromHistory = useCallback((item, clearFavorites = false) => {
|
||||||
|
- historyStore.current.deleteHistory(item, clearFavorites);
|
||||||
|
- setItems(historyStore.current.queries);
|
||||||
|
- }, []);
|
||||||
|
+ const [items, setItems] = useState(() => historyStore.queries || []);
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
- addToHistory,
|
||||||
|
- editLabel,
|
||||||
|
+ addToHistory(operation) {
|
||||||
|
+ historyStore.updateHistory(operation);
|
||||||
|
+ setItems(historyStore.queries);
|
||||||
|
+ },
|
||||||
|
+ editLabel(operation, index) {
|
||||||
|
+ historyStore.editLabel(operation, index);
|
||||||
|
+ setItems(historyStore.queries);
|
||||||
|
+ },
|
||||||
|
items,
|
||||||
|
- toggleFavorite,
|
||||||
|
- setActive,
|
||||||
|
- deleteFromHistory
|
||||||
|
+ toggleFavorite(operation) {
|
||||||
|
+ historyStore.toggleFavorite(operation);
|
||||||
|
+ setItems(historyStore.queries);
|
||||||
|
+ },
|
||||||
|
+ setActive: (item) => item,
|
||||||
|
+ deleteFromHistory(item, clearFavorites) {
|
||||||
|
+ historyStore.deleteHistory(item, clearFavorites);
|
||||||
|
+ setItems(historyStore.queries);
|
||||||
|
+ }
|
||||||
|
}),
|
||||||
|
- [
|
||||||
|
- addToHistory,
|
||||||
|
- editLabel,
|
||||||
|
- items,
|
||||||
|
- toggleFavorite,
|
||||||
|
- setActive,
|
||||||
|
- deleteFromHistory
|
||||||
|
- ]
|
||||||
|
+ [items, historyStore]
|
||||||
|
);
|
||||||
|
- return /* @__PURE__ */ jsx(HistoryContext.Provider, { value, children: props.children });
|
||||||
|
+ return /* @__PURE__ */ jsx(HistoryContext.Provider, { value, children });
|
||||||
|
}
|
||||||
|
const useHistoryContext = createContextHook(HistoryContext);
|
||||||
|
const DEFAULT_HISTORY_LENGTH = 20;
|
||||||
|
@@ -714,7 +688,8 @@ function ExecutionContextProvider({
|
||||||
|
fetcher,
|
||||||
|
getDefaultFieldNames,
|
||||||
|
children,
|
||||||
|
- operationName
|
||||||
|
+ operationName,
|
||||||
|
+ onModifyHeaders
|
||||||
|
}) {
|
||||||
|
if (!fetcher) {
|
||||||
|
throw new TypeError(
|
||||||
|
@@ -792,6 +767,9 @@ function ExecutionContextProvider({
|
||||||
|
}
|
||||||
|
setResponse("");
|
||||||
|
setIsFetching(true);
|
||||||
|
+ if (onModifyHeaders) {
|
||||||
|
+ headers = await onModifyHeaders(headers);
|
||||||
|
+ }
|
||||||
|
const opName = operationName ?? queryEditor.operationName ?? void 0;
|
||||||
|
history == null ? void 0 : history.addToHistory({
|
||||||
|
query,
|
||||||
|
@@ -999,9 +977,9 @@ function mergeIncrementalResult(executionResult, incrementalResult) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+const isMacOs = typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
|
||||||
|
const DEFAULT_EDITOR_THEME = "graphiql";
|
||||||
|
const DEFAULT_KEY_MAP = "sublime";
|
||||||
|
-const isMacOs = typeof navigator !== "undefined" && navigator.platform.toLowerCase().indexOf("mac") === 0;
|
||||||
|
const commonKeys = {
|
||||||
|
// Persistent search box in Query Editor
|
||||||
|
[isMacOs ? "Cmd-F" : "Ctrl-F"]: "findPersistent",
|
||||||
|
@@ -1599,7 +1577,7 @@ function Search() {
|
||||||
|
onFocus: handleFocus,
|
||||||
|
onBlur: handleFocus,
|
||||||
|
onChange: (event) => setSearchValue(event.target.value),
|
||||||
|
- placeholder: "⌘ K",
|
||||||
|
+ placeholder: `${isMacOs ? "⌘" : "Ctrl"} K`,
|
||||||
|
ref: inputRef,
|
||||||
|
value: searchValue,
|
||||||
|
"data-cy": "doc-explorer-input"
|
||||||
|
@@ -3063,14 +3041,16 @@ function useSetEditorValues({
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function createTab({
|
||||||
|
+ id,
|
||||||
|
+ title,
|
||||||
|
query = null,
|
||||||
|
variables = null,
|
||||||
|
headers = null
|
||||||
|
-} = {}) {
|
||||||
|
+}) {
|
||||||
|
return {
|
||||||
|
- id: guid(),
|
||||||
|
+ id: id || guid(),
|
||||||
|
hash: hashFromTabContents({ query, variables, headers }),
|
||||||
|
- title: query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
|
||||||
|
+ title: title || query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
headers,
|
||||||
|
@@ -3088,8 +3068,7 @@ function setPropertiesInActiveTab(state, partialTab) {
|
||||||
|
const newTab = { ...tab, ...partialTab };
|
||||||
|
return {
|
||||||
|
...newTab,
|
||||||
|
- hash: hashFromTabContents(newTab),
|
||||||
|
- title: newTab.operationName || (newTab.query ? fuzzyExtractOperationName(newTab.query) : void 0) || DEFAULT_TITLE
|
||||||
|
+ hash: hashFromTabContents(newTab)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
@@ -3311,32 +3290,36 @@ function EditorContextProvider(props) {
|
||||||
|
responseEditor,
|
||||||
|
defaultHeaders
|
||||||
|
});
|
||||||
|
- const addTab = useCallback(() => {
|
||||||
|
- setTabState((current) => {
|
||||||
|
- const updatedValues = synchronizeActiveTabValues(current);
|
||||||
|
- const updated = {
|
||||||
|
- tabs: [
|
||||||
|
- ...updatedValues.tabs,
|
||||||
|
- createTab({
|
||||||
|
- headers: defaultHeaders,
|
||||||
|
- query: defaultQuery ?? DEFAULT_QUERY
|
||||||
|
- })
|
||||||
|
- ],
|
||||||
|
- activeTabIndex: updatedValues.tabs.length
|
||||||
|
- };
|
||||||
|
- storeTabs(updated);
|
||||||
|
- setEditorValues(updated.tabs[updated.activeTabIndex]);
|
||||||
|
- onTabChange == null ? void 0 : onTabChange(updated);
|
||||||
|
- return updated;
|
||||||
|
- });
|
||||||
|
- }, [
|
||||||
|
- defaultHeaders,
|
||||||
|
- defaultQuery,
|
||||||
|
- onTabChange,
|
||||||
|
- setEditorValues,
|
||||||
|
- storeTabs,
|
||||||
|
- synchronizeActiveTabValues
|
||||||
|
- ]);
|
||||||
|
+ const addTab = useCallback(
|
||||||
|
+ (_tabState) => {
|
||||||
|
+ setTabState((current) => {
|
||||||
|
+ const updatedValues = synchronizeActiveTabValues(current);
|
||||||
|
+ const updated = {
|
||||||
|
+ tabs: [
|
||||||
|
+ ...updatedValues.tabs,
|
||||||
|
+ createTab({
|
||||||
|
+ ..._tabState,
|
||||||
|
+ headers: defaultHeaders,
|
||||||
|
+ query: defaultQuery ?? DEFAULT_QUERY
|
||||||
|
+ })
|
||||||
|
+ ],
|
||||||
|
+ activeTabIndex: updatedValues.tabs.length
|
||||||
|
+ };
|
||||||
|
+ storeTabs(updated);
|
||||||
|
+ setEditorValues(updated.tabs[updated.activeTabIndex]);
|
||||||
|
+ onTabChange == null ? void 0 : onTabChange(updated);
|
||||||
|
+ return updated;
|
||||||
|
+ });
|
||||||
|
+ },
|
||||||
|
+ [
|
||||||
|
+ defaultHeaders,
|
||||||
|
+ defaultQuery,
|
||||||
|
+ onTabChange,
|
||||||
|
+ setEditorValues,
|
||||||
|
+ storeTabs,
|
||||||
|
+ synchronizeActiveTabValues
|
||||||
|
+ ]
|
||||||
|
+ );
|
||||||
|
const changeTab = useCallback(
|
||||||
|
(index) => {
|
||||||
|
setTabState((current) => {
|
||||||
|
@@ -3432,6 +3415,7 @@ function EditorContextProvider(props) {
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
...tabState,
|
||||||
|
+ setTabState,
|
||||||
|
addTab,
|
||||||
|
changeTab,
|
||||||
|
moveTab,
|
||||||
|
@@ -3743,9 +3727,10 @@ function GraphiQLProvider({
|
||||||
|
storage,
|
||||||
|
validationRules,
|
||||||
|
variables,
|
||||||
|
- visiblePlugin
|
||||||
|
+ visiblePlugin,
|
||||||
|
+ onModifyHeaders
|
||||||
|
}) {
|
||||||
|
- return /* @__PURE__ */ jsx(StorageContextProvider, { storage, children: /* @__PURE__ */ jsx(HistoryContextProvider, { maxHistoryLength, children: /* @__PURE__ */ jsx(
|
||||||
|
+ return /* @__PURE__ */ jsx(StorageContextProvider, { storage, children: /* @__PURE__ */ jsx(
|
||||||
|
EditorContextProvider,
|
||||||
|
{
|
||||||
|
defaultQuery,
|
||||||
|
@@ -3776,6 +3761,7 @@ function GraphiQLProvider({
|
||||||
|
getDefaultFieldNames,
|
||||||
|
fetcher,
|
||||||
|
operationName,
|
||||||
|
+ onModifyHeaders,
|
||||||
|
children: /* @__PURE__ */ jsx(ExplorerContextProvider, { children: /* @__PURE__ */ jsx(
|
||||||
|
PluginContextProvider,
|
||||||
|
{
|
||||||
|
@@ -3790,7 +3776,7 @@ function GraphiQLProvider({
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
- ) }) });
|
||||||
|
+ ) });
|
||||||
|
}
|
||||||
|
function useTheme(defaultTheme = null) {
|
||||||
|
const storageContext = useStorageContext();
|
||||||
|
@@ -4200,6 +4186,7 @@ export {
|
||||||
|
TypeLink,
|
||||||
|
UnStyledButton,
|
||||||
|
VariableEditor,
|
||||||
|
+ isMacOs,
|
||||||
|
useAutoCompleteLeafs,
|
||||||
|
useCopyQuery,
|
||||||
|
useDragResize,
|
||||||
|
diff --git a/dist/types/editor/context.d.ts b/dist/types/editor/context.d.ts
|
||||||
|
index 199db8a294f8132d46470498870adbdf9fdc83af..d8901fe0d50db17db36a502dcf69d5f69efb84a1 100644
|
||||||
|
--- a/dist/types/editor/context.d.ts
|
||||||
|
+++ b/dist/types/editor/context.d.ts
|
||||||
|
@@ -1,6 +1,6 @@
|
||||||
|
import { DocumentNode, FragmentDefinitionNode, OperationDefinitionNode, ValidationRule } from 'graphql';
|
||||||
|
import { VariableToType } from 'graphql-language-service';
|
||||||
|
-import { ReactNode } from 'react';
|
||||||
|
+import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||||
|
import { TabDefinition, TabsState, TabState } from './tabs';
|
||||||
|
import { CodeMirrorEditor } from './types';
|
||||||
|
export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
|
||||||
|
@@ -10,10 +10,11 @@ export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
|
||||||
|
variableToType: VariableToType | null;
|
||||||
|
};
|
||||||
|
export declare type EditorContextType = TabsState & {
|
||||||
|
+ setTabState: Dispatch<SetStateAction<TabsState>>;
|
||||||
|
/**
|
||||||
|
* Add a new tab.
|
||||||
|
*/
|
||||||
|
- addTab(): void;
|
||||||
|
+ addTab(tabState?: Pick<TabState, 'id' | 'query' | 'variables' | 'headers' | 'title'>): void;
|
||||||
|
/**
|
||||||
|
* Switch to a different tab.
|
||||||
|
* @param index The index of the tab that should be switched to.
|
||||||
|
@@ -38,7 +39,7 @@ export declare type EditorContextType = TabsState & {
|
||||||
|
* @param partialTab A partial tab state object that will override the
|
||||||
|
* current values. The properties `id`, `hash` and `title` cannot be changed.
|
||||||
|
*/
|
||||||
|
- updateActiveTabValues(partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): void;
|
||||||
|
+ updateActiveTabValues(partialTab: Partial<Omit<TabState, 'hash'>>): void;
|
||||||
|
/**
|
||||||
|
* The CodeMirror editor instance for the headers editor.
|
||||||
|
*/
|
||||||
|
diff --git a/dist/types/editor/tabs.d.ts b/dist/types/editor/tabs.d.ts
|
||||||
|
index 28704a9c1c6e22fa75986de8591759e13035c8c5..5204d2b25198f89da9bba70804656f02799c7df6 100644
|
||||||
|
--- a/dist/types/editor/tabs.d.ts
|
||||||
|
+++ b/dist/types/editor/tabs.d.ts
|
||||||
|
@@ -90,7 +90,7 @@ export declare function useSetEditorValues({ queryEditor, variableEditor, header
|
||||||
|
headers?: string | null | undefined;
|
||||||
|
response: string | null;
|
||||||
|
}) => void;
|
||||||
|
-export declare function createTab({ query, variables, headers, }?: Partial<TabDefinition>): TabState;
|
||||||
|
+export declare function createTab({ id, title, query, variables, headers, }: Partial<TabDefinition & Pick<TabState, 'id' | 'title'>>): TabState;
|
||||||
|
export declare function setPropertiesInActiveTab(state: TabsState, partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): TabsState;
|
||||||
|
export declare function fuzzyExtractOperationName(str: string): string | null;
|
||||||
|
export declare function clearHeadersFromTabs(storage: StorageAPI | null): void;
|
||||||
|
diff --git a/dist/types/execution.d.ts b/dist/types/execution.d.ts
|
||||||
|
index 2d458001265d925ed0323a10aecbefdb7e6d0b4e..eb024cf197f13bfaa67423f5751c7cad7d0664bc 100644
|
||||||
|
--- a/dist/types/execution.d.ts
|
||||||
|
+++ b/dist/types/execution.d.ts
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-import { Fetcher } from '@graphiql/toolkit';
|
||||||
|
+import { Fetcher, MaybePromise } from '@graphiql/toolkit';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { UseAutoCompleteLeafsArgs } from './editor/hooks';
|
||||||
|
export declare type ExecutionContextType = {
|
||||||
|
@@ -45,8 +45,13 @@ export declare type ExecutionContextProviderProps = Pick<UseAutoCompleteLeafsArg
|
||||||
|
* This prop sets the operation name that is passed with a GraphQL request.
|
||||||
|
*/
|
||||||
|
operationName?: string;
|
||||||
|
+ /**
|
||||||
|
+ * Modify headers before execution
|
||||||
|
+ * e.g. for interpolating headers values `"myKey": "{{valueToInterpolate}}"`
|
||||||
|
+ */
|
||||||
|
+ onModifyHeaders?: (headers?: Record<string, unknown>) => MaybePromise<Record<string, unknown>>;
|
||||||
|
};
|
||||||
|
-export declare function ExecutionContextProvider({ fetcher, getDefaultFieldNames, children, operationName, }: ExecutionContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
||||||
|
+export declare function ExecutionContextProvider({ fetcher, getDefaultFieldNames, children, operationName, onModifyHeaders, }: ExecutionContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
||||||
|
export declare const useExecutionContext: {
|
||||||
|
(options: {
|
||||||
|
nonNull: true;
|
||||||
|
diff --git a/dist/types/history/context.d.ts b/dist/types/history/context.d.ts
|
||||||
|
index f2699b344d27806094c0e5d62d914e5618dcf4db..9e6e3c6cdfded41af49c4c15c8d0be100e896bb0 100644
|
||||||
|
--- a/dist/types/history/context.d.ts
|
||||||
|
+++ b/dist/types/history/context.d.ts
|
||||||
|
@@ -76,7 +76,7 @@ export declare type HistoryContextProviderProps = {
|
||||||
|
* any additional props they added for their needs (i.e., build their own functions that may save
|
||||||
|
* to a backend instead of localStorage and might need an id property added to the QueryStoreItem)
|
||||||
|
*/
|
||||||
|
-export declare function HistoryContextProvider(props: HistoryContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
||||||
|
+export declare function HistoryContextProvider({ maxHistoryLength, children, }: HistoryContextProviderProps): import("react/jsx-runtime").JSX.Element;
|
||||||
|
export declare const useHistoryContext: {
|
||||||
|
(options: {
|
||||||
|
nonNull: true;
|
||||||
|
diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts
|
||||||
|
index 26ef2a2a07dcdf29f868067d32a0f5ff7981d8e6..28d9620636bab2221239ab8b87505425a0468b5f 100644
|
||||||
|
--- a/dist/types/index.d.ts
|
||||||
|
+++ b/dist/types/index.d.ts
|
||||||
|
@@ -8,6 +8,7 @@ export { SchemaContext, SchemaContextProvider, useSchemaContext, } from './schem
|
||||||
|
export { StorageContext, StorageContextProvider, useStorageContext, } from './storage';
|
||||||
|
export { useTheme } from './theme';
|
||||||
|
export { useDragResize } from './utility/resize';
|
||||||
|
+export { isMacOs } from './utility/is-macos';
|
||||||
|
export * from './icons';
|
||||||
|
export * from './ui';
|
||||||
|
export * from './toolbar';
|
||||||
|
diff --git a/dist/types/provider.d.ts b/dist/types/provider.d.ts
|
||||||
|
index e95c73f0b8c7cdfaece528e5f411ffd29862d490..d0d1e80a13da5d22abbcb4d6e052e91323fcc86f 100644
|
||||||
|
--- a/dist/types/provider.d.ts
|
||||||
|
+++ b/dist/types/provider.d.ts
|
||||||
|
@@ -6,4 +6,4 @@ import { PluginContextProviderProps } from './plugin';
|
||||||
|
import { SchemaContextProviderProps } from './schema';
|
||||||
|
import { StorageContextProviderProps } from './storage';
|
||||||
|
export declare type GraphiQLProviderProps = EditorContextProviderProps & ExecutionContextProviderProps & ExplorerContextProviderProps & HistoryContextProviderProps & PluginContextProviderProps & SchemaContextProviderProps & StorageContextProviderProps;
|
||||||
|
-export declare function GraphiQLProvider({ children, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, }: GraphiQLProviderProps): import("react/jsx-runtime").JSX.Element;
|
||||||
|
+export declare function GraphiQLProvider({ children, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, onModifyHeaders, }: GraphiQLProviderProps): import("react/jsx-runtime").JSX.Element;
|
||||||
|
diff --git a/dist/types/storage.d.ts b/dist/types/storage.d.ts
|
||||||
|
index c4c98ab5c3cd32837109d9d20d4808ad6793fd3f..0a1257b6e041d42068bffb5f332855372b89ea88 100644
|
||||||
|
--- a/dist/types/storage.d.ts
|
||||||
|
+++ b/dist/types/storage.d.ts
|
||||||
|
@@ -6,7 +6,7 @@ export declare type StorageContextProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* Provide a custom storage API.
|
||||||
|
- * @default `localStorage``
|
||||||
|
+ * @default `localStorage`
|
||||||
|
* @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#storage-2|API docs}
|
||||||
|
* for details on the required interface.
|
||||||
|
*/
|
||||||
|
diff --git a/dist/types/utility/is-macos.d.ts b/dist/types/utility/is-macos.d.ts
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000000000000000000000000000000000..5f05699dde4723cbd446e914900dd9e7ff41ae70
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/dist/types/utility/is-macos.d.ts
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+export declare const isMacOs: boolean;
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
|
||||||
index 567480ba5ccc30619db2eb8e6da8d618ddb5e891..c49c573893a26897062cda2af9896bc69881db2a 100644
|
|
||||||
--- a/dist/index.mjs
|
|
||||||
+++ b/dist/index.mjs
|
|
||||||
@@ -3056,14 +3056,16 @@ function useSetEditorValues({
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function createTab({
|
|
||||||
+ id,
|
|
||||||
+ title,
|
|
||||||
query = null,
|
|
||||||
variables = null,
|
|
||||||
headers = null
|
|
||||||
-} = {}) {
|
|
||||||
+}) {
|
|
||||||
return {
|
|
||||||
- id: guid(),
|
|
||||||
+ id: id || guid(),
|
|
||||||
hash: hashFromTabContents({ query, variables, headers }),
|
|
||||||
- title: query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
|
|
||||||
+ title: title || query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
|
|
||||||
query,
|
|
||||||
variables,
|
|
||||||
headers,
|
|
||||||
@@ -3081,8 +3083,7 @@ function setPropertiesInActiveTab(state, partialTab) {
|
|
||||||
const newTab = { ...tab, ...partialTab };
|
|
||||||
return {
|
|
||||||
...newTab,
|
|
||||||
- hash: hashFromTabContents(newTab),
|
|
||||||
- title: newTab.operationName || (newTab.query ? fuzzyExtractOperationName(newTab.query) : void 0) || DEFAULT_TITLE
|
|
||||||
+ hash: hashFromTabContents(newTab)
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
@@ -3304,25 +3305,31 @@ function EditorContextProvider(props) {
|
|
||||||
responseEditor,
|
|
||||||
defaultHeaders
|
|
||||||
});
|
|
||||||
- const addTab = useCallback(() => {
|
|
||||||
- setTabState((current) => {
|
|
||||||
- const updatedValues = synchronizeActiveTabValues(current);
|
|
||||||
- const updated = {
|
|
||||||
- tabs: [...updatedValues.tabs, createTab({ headers: defaultHeaders })],
|
|
||||||
- activeTabIndex: updatedValues.tabs.length
|
|
||||||
- };
|
|
||||||
- storeTabs(updated);
|
|
||||||
- setEditorValues(updated.tabs[updated.activeTabIndex]);
|
|
||||||
- onTabChange == null ? void 0 : onTabChange(updated);
|
|
||||||
- return updated;
|
|
||||||
- });
|
|
||||||
- }, [
|
|
||||||
- defaultHeaders,
|
|
||||||
- onTabChange,
|
|
||||||
- setEditorValues,
|
|
||||||
- storeTabs,
|
|
||||||
- synchronizeActiveTabValues
|
|
||||||
- ]);
|
|
||||||
+ const addTab = useCallback(
|
|
||||||
+ (_tabState) => {
|
|
||||||
+ setTabState((current) => {
|
|
||||||
+ const updatedValues = synchronizeActiveTabValues(current);
|
|
||||||
+ const updated = {
|
|
||||||
+ tabs: [
|
|
||||||
+ ...updatedValues.tabs,
|
|
||||||
+ createTab({ ..._tabState, headers: defaultHeaders })
|
|
||||||
+ ],
|
|
||||||
+ activeTabIndex: updatedValues.tabs.length
|
|
||||||
+ };
|
|
||||||
+ storeTabs(updated);
|
|
||||||
+ setEditorValues(updated.tabs[updated.activeTabIndex]);
|
|
||||||
+ onTabChange == null ? void 0 : onTabChange(updated);
|
|
||||||
+ return updated;
|
|
||||||
+ });
|
|
||||||
+ },
|
|
||||||
+ [
|
|
||||||
+ defaultHeaders,
|
|
||||||
+ onTabChange,
|
|
||||||
+ setEditorValues,
|
|
||||||
+ storeTabs,
|
|
||||||
+ synchronizeActiveTabValues
|
|
||||||
+ ]
|
|
||||||
+ );
|
|
||||||
const changeTab = useCallback(
|
|
||||||
(index) => {
|
|
||||||
setTabState((current) => {
|
|
||||||
@@ -3418,6 +3425,7 @@ function EditorContextProvider(props) {
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({
|
|
||||||
...tabState,
|
|
||||||
+ setTabState,
|
|
||||||
addTab,
|
|
||||||
changeTab,
|
|
||||||
moveTab,
|
|
||||||
diff --git a/dist/types/editor/context.d.ts b/dist/types/editor/context.d.ts
|
|
||||||
index 199db8a294f8132d46470498870adbdf9fdc83af..d8901fe0d50db17db36a502dcf69d5f69efb84a1 100644
|
|
||||||
--- a/dist/types/editor/context.d.ts
|
|
||||||
+++ b/dist/types/editor/context.d.ts
|
|
||||||
@@ -1,6 +1,6 @@
|
|
||||||
import { DocumentNode, FragmentDefinitionNode, OperationDefinitionNode, ValidationRule } from 'graphql';
|
|
||||||
import { VariableToType } from 'graphql-language-service';
|
|
||||||
-import { ReactNode } from 'react';
|
|
||||||
+import { Dispatch, ReactNode, SetStateAction } from 'react';
|
|
||||||
import { TabDefinition, TabsState, TabState } from './tabs';
|
|
||||||
import { CodeMirrorEditor } from './types';
|
|
||||||
export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
|
|
||||||
@@ -10,10 +10,11 @@ export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
|
|
||||||
variableToType: VariableToType | null;
|
|
||||||
};
|
|
||||||
export declare type EditorContextType = TabsState & {
|
|
||||||
+ setTabState: Dispatch<SetStateAction<TabsState>>;
|
|
||||||
/**
|
|
||||||
* Add a new tab.
|
|
||||||
*/
|
|
||||||
- addTab(): void;
|
|
||||||
+ addTab(tabState?: Pick<TabState, 'id' | 'query' | 'variables' | 'headers' | 'title'>): void;
|
|
||||||
/**
|
|
||||||
* Switch to a different tab.
|
|
||||||
* @param index The index of the tab that should be switched to.
|
|
||||||
@@ -38,7 +39,7 @@ export declare type EditorContextType = TabsState & {
|
|
||||||
* @param partialTab A partial tab state object that will override the
|
|
||||||
* current values. The properties `id`, `hash` and `title` cannot be changed.
|
|
||||||
*/
|
|
||||||
- updateActiveTabValues(partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): void;
|
|
||||||
+ updateActiveTabValues(partialTab: Partial<Omit<TabState, 'hash'>>): void;
|
|
||||||
/**
|
|
||||||
* The CodeMirror editor instance for the headers editor.
|
|
||||||
*/
|
|
||||||
diff --git a/dist/types/editor/tabs.d.ts b/dist/types/editor/tabs.d.ts
|
|
||||||
index 28704a9c1c6e22fa75986de8591759e13035c8c5..5204d2b25198f89da9bba70804656f02799c7df6 100644
|
|
||||||
--- a/dist/types/editor/tabs.d.ts
|
|
||||||
+++ b/dist/types/editor/tabs.d.ts
|
|
||||||
@@ -90,7 +90,7 @@ export declare function useSetEditorValues({ queryEditor, variableEditor, header
|
|
||||||
headers?: string | null | undefined;
|
|
||||||
response: string | null;
|
|
||||||
}) => void;
|
|
||||||
-export declare function createTab({ query, variables, headers, }?: Partial<TabDefinition>): TabState;
|
|
||||||
+export declare function createTab({ id, title, query, variables, headers, }: Partial<TabDefinition & Pick<TabState, 'id' | 'title'>>): TabState;
|
|
||||||
export declare function setPropertiesInActiveTab(state: TabsState, partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): TabsState;
|
|
||||||
export declare function fuzzyExtractOperationName(str: string): string | null;
|
|
||||||
export declare function clearHeadersFromTabs(storage: StorageAPI | null): void;
|
|
||||||
59
patches/graphiql.patch
Normal file
59
patches/graphiql.patch
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
diff --git a/dist/index.d.ts b/dist/index.d.ts
|
||||||
|
index d0d893ea0caffb6c1c70c5f95aed8ca49bc74701..ba3c02801e958c66fde9b813821e5a608f49b1cf 100644
|
||||||
|
--- a/dist/index.d.ts
|
||||||
|
+++ b/dist/index.d.ts
|
||||||
|
@@ -14,7 +14,7 @@ declare type AddSuffix<Obj extends Record<string, any>, Suffix extends string> =
|
||||||
|
[Key in keyof Obj as `${string & Key}${Suffix}`]: Obj[Key];
|
||||||
|
};
|
||||||
|
|
||||||
|
-export declare function GraphiQL({ dangerouslyAssumeSchemaIsValid, confirmCloseTab, defaultQuery, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, defaultHeaders, ...props }: GraphiQLProps): JSX_2.Element;
|
||||||
|
+export declare function GraphiQL({ dangerouslyAssumeSchemaIsValid, confirmCloseTab, defaultQuery, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, defaultHeaders, onModifyHeaders, ...props }: GraphiQLProps): JSX_2.Element;
|
||||||
|
|
||||||
|
export declare namespace GraphiQL {
|
||||||
|
var Logo: typeof GraphiQLLogo;
|
||||||
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
|
index cf1a9036b4b35b7918da09ead6977e1e77724b8a..896dadb9c22b36ceee99776b87684f9e3899023d 100644
|
||||||
|
--- a/dist/index.mjs
|
||||||
|
+++ b/dist/index.mjs
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-import { GraphiQLProvider, useEditorContext, useExecutionContext, useSchemaContext, useStorageContext, usePluginContext, useTheme, useDragResize, Tooltip, UnStyledButton, ReloadIcon, KeyboardShortcutIcon, SettingsIcon, Tabs, Tab, PlusIcon, QueryEditor, ExecuteButton, ChevronUpIcon, ChevronDownIcon, VariableEditor, HeaderEditor, Spinner, ResponseEditor, Dialog, ButtonGroup, Button, useCopyQuery, useMergeQuery, usePrettifyEditors, ToolbarButton, PrettifyIcon, MergeIcon, CopyIcon } from "@graphiql/react";
|
||||||
|
+import { GraphiQLProvider, useEditorContext, useExecutionContext, useSchemaContext, useStorageContext, usePluginContext, useTheme, useDragResize, Tooltip, UnStyledButton, ReloadIcon, KeyboardShortcutIcon, SettingsIcon, Tabs, Tab, PlusIcon, QueryEditor, ExecuteButton, ChevronUpIcon, ChevronDownIcon, VariableEditor, HeaderEditor, Spinner, ResponseEditor, Dialog, ButtonGroup, Button, useCopyQuery, useMergeQuery, usePrettifyEditors, ToolbarButton, PrettifyIcon, MergeIcon, CopyIcon, isMacOs } from "@graphiql/react";
|
||||||
|
import { GraphiQLProvider as GraphiQLProvider2 } from "@graphiql/react";
|
||||||
|
import React, { version, useMemo, useEffect, useState, Children, cloneElement, useCallback, Fragment } from "react";
|
||||||
|
const majorVersion = parseInt(version.slice(0, 2), 10);
|
||||||
|
@@ -39,6 +39,7 @@ function GraphiQL({
|
||||||
|
variables,
|
||||||
|
visiblePlugin,
|
||||||
|
defaultHeaders,
|
||||||
|
+ onModifyHeaders,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
var _a, _b;
|
||||||
|
@@ -85,7 +86,8 @@ function GraphiQL({
|
||||||
|
shouldPersistHeaders,
|
||||||
|
storage,
|
||||||
|
validationRules,
|
||||||
|
- variables
|
||||||
|
+ variables,
|
||||||
|
+ onModifyHeaders
|
||||||
|
},
|
||||||
|
/* @__PURE__ */ React.createElement(
|
||||||
|
GraphiQLInterface,
|
||||||
|
@@ -398,7 +400,7 @@ function GraphiQLInterface(props) {
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
className: "graphiql-tab-add",
|
||||||
|
- onClick: handleAddTab,
|
||||||
|
+ onClick: () => handleAddTab(),
|
||||||
|
"aria-label": "New tab"
|
||||||
|
},
|
||||||
|
/* @__PURE__ */ React.createElement(PlusIcon, { "aria-hidden": "true" })
|
||||||
|
@@ -602,7 +604,7 @@ function GraphiQLInterface(props) {
|
||||||
|
)) : null
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
-const modifier = typeof navigator !== "undefined" && navigator.platform.toLowerCase().indexOf("mac") === 0 ? "Cmd" : "Ctrl";
|
||||||
|
+const modifier = isMacOs ? "⌘" : "Ctrl";
|
||||||
|
const SHORT_KEYS = Object.entries({
|
||||||
|
"Search in editor": [modifier, "F"],
|
||||||
|
"Search in documentation": [modifier, "K"],
|
||||||
225
pnpm-lock.yaml
225
pnpm-lock.yaml
|
|
@ -16,9 +16,12 @@ patchedDependencies:
|
||||||
'@apollo/federation@0.38.1':
|
'@apollo/federation@0.38.1':
|
||||||
hash: rjgakkkphrejw6qrtph4ar24zq
|
hash: rjgakkkphrejw6qrtph4ar24zq
|
||||||
path: patches/@apollo__federation@0.38.1.patch
|
path: patches/@apollo__federation@0.38.1.patch
|
||||||
'@graphiql/react@1.0.0-alpha.3':
|
'@fastify/vite':
|
||||||
hash: gnbo5nw2wgehkfq2yrmuhrx4im
|
hash: wz23vdqq6qtsz64wb433afnvou
|
||||||
path: patches/@graphiql__react@1.0.0-alpha.3.patch
|
path: patches/@fastify__vite.patch
|
||||||
|
'@graphiql/react':
|
||||||
|
hash: bru5she67j343rpipomank3vn4
|
||||||
|
path: patches/@graphiql__react.patch
|
||||||
'@graphql-eslint/eslint-plugin@3.20.1':
|
'@graphql-eslint/eslint-plugin@3.20.1':
|
||||||
hash: n437g5o7zq7pnxdxldn52uql2q
|
hash: n437g5o7zq7pnxdxldn52uql2q
|
||||||
path: patches/@graphql-eslint__eslint-plugin@3.20.1.patch
|
path: patches/@graphql-eslint__eslint-plugin@3.20.1.patch
|
||||||
|
|
@ -40,6 +43,9 @@ patchedDependencies:
|
||||||
got@14.4.5:
|
got@14.4.5:
|
||||||
hash: b6pwqmrs3qqykctltsasvrfwti
|
hash: b6pwqmrs3qqykctltsasvrfwti
|
||||||
path: patches/got@14.4.5.patch
|
path: patches/got@14.4.5.patch
|
||||||
|
graphiql:
|
||||||
|
hash: yjzkcog7ut7wshk4npre67txki
|
||||||
|
path: patches/graphiql.patch
|
||||||
mjml-core@4.14.0:
|
mjml-core@4.14.0:
|
||||||
hash: zxxsxbqejjmcwuzpigutzzq6wa
|
hash: zxxsxbqejjmcwuzpigutzzq6wa
|
||||||
path: patches/mjml-core@4.14.0.patch
|
path: patches/mjml-core@4.14.0.patch
|
||||||
|
|
@ -689,6 +695,12 @@ importers:
|
||||||
'@hive/webhooks':
|
'@hive/webhooks':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../webhooks
|
version: link:../webhooks
|
||||||
|
'@nodesecure/i18n':
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
|
'@nodesecure/js-x-ray':
|
||||||
|
specifier: 8.0.0
|
||||||
|
version: 8.0.0
|
||||||
'@octokit/app':
|
'@octokit/app':
|
||||||
specifier: 14.1.0
|
specifier: 14.1.0
|
||||||
version: 14.1.0
|
version: 14.1.0
|
||||||
|
|
@ -1661,14 +1673,14 @@ importers:
|
||||||
specifier: 7.0.4
|
specifier: 7.0.4
|
||||||
version: 7.0.4
|
version: 7.0.4
|
||||||
'@fastify/vite':
|
'@fastify/vite':
|
||||||
specifier: 6.0.7
|
specifier: 6.0.6
|
||||||
version: 6.0.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)
|
version: 6.0.6(patch_hash=wz23vdqq6qtsz64wb433afnvou)(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)
|
||||||
'@graphiql/plugin-explorer':
|
'@graphiql/plugin-explorer':
|
||||||
specifier: 4.0.0-alpha.2
|
specifier: 4.0.0-alpha.2
|
||||||
version: 4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@graphiql/react':
|
'@graphiql/react':
|
||||||
specifier: 1.0.0-alpha.3
|
specifier: 1.0.0-alpha.4
|
||||||
version: 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@graphiql/toolkit':
|
'@graphiql/toolkit':
|
||||||
specifier: 0.9.1
|
specifier: 0.9.1
|
||||||
version: 0.9.1(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)
|
version: 0.9.1(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)
|
||||||
|
|
@ -1813,6 +1825,9 @@ importers:
|
||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: 10.45.2
|
specifier: 10.45.2
|
||||||
version: 10.45.2
|
version: 10.45.2
|
||||||
|
'@types/crypto-js':
|
||||||
|
specifier: ^4.2.2
|
||||||
|
version: 4.2.2
|
||||||
'@types/dompurify':
|
'@types/dompurify':
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
|
|
@ -1858,6 +1873,9 @@ importers:
|
||||||
cmdk:
|
cmdk:
|
||||||
specifier: 0.2.1
|
specifier: 0.2.1
|
||||||
version: 0.2.1(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 0.2.1(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
crypto-js:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.0
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: 4.1.0
|
specifier: 4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
|
@ -1880,11 +1898,11 @@ importers:
|
||||||
specifier: 2.4.6
|
specifier: 2.4.6
|
||||||
version: 2.4.6(react@18.3.1)
|
version: 2.4.6(react@18.3.1)
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: 11.15.0
|
specifier: 11.11.17
|
||||||
version: 11.15.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 11.11.17(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
graphiql:
|
graphiql:
|
||||||
specifier: 4.0.0-alpha.4
|
specifier: 4.0.0-alpha.5
|
||||||
version: 4.0.0-alpha.4(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 4.0.0-alpha.5(patch_hash=yjzkcog7ut7wshk4npre67txki)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
graphql:
|
graphql:
|
||||||
specifier: 16.9.0
|
specifier: 16.9.0
|
||||||
version: 16.9.0
|
version: 16.9.0
|
||||||
|
|
@ -3940,8 +3958,8 @@ packages:
|
||||||
'@fastify/static@7.0.4':
|
'@fastify/static@7.0.4':
|
||||||
resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==}
|
resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==}
|
||||||
|
|
||||||
'@fastify/vite@6.0.7':
|
'@fastify/vite@6.0.6':
|
||||||
resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==}
|
resolution: {integrity: sha512-FsWJC92murm5tjeTezTTvMLyZido/ZWy0wYWpVkh/bDe1gAUAabYLB7Vp8hokXGsRE/mOpqYVsRDAKENY2qPUQ==}
|
||||||
bundledDependencies: []
|
bundledDependencies: []
|
||||||
|
|
||||||
'@floating-ui/core@1.2.6':
|
'@floating-ui/core@1.2.6':
|
||||||
|
|
@ -3988,8 +4006,8 @@ packages:
|
||||||
react: ^16.8.0 || ^17 || ^18
|
react: ^16.8.0 || ^17 || ^18
|
||||||
react-dom: ^16.8.0 || ^17 || ^18
|
react-dom: ^16.8.0 || ^17 || ^18
|
||||||
|
|
||||||
'@graphiql/react@1.0.0-alpha.3':
|
'@graphiql/react@1.0.0-alpha.4':
|
||||||
resolution: {integrity: sha512-9WPfC7I9xO1qC/dKaYwVe3UbPJvdjU+fxoUW2Id0mIljkD7LXnVUnzBQMB1SY4JrRJX3I0nQPbHXzKvAYSpnjw==}
|
resolution: {integrity: sha512-psie5qQNVlklXAhNPD8sIRtpNDzJfNzzZ5EH0ofrJ8AeVcj+DYmIqxRWw5zvjDAtNTKyOAJSCpQdCNDCCi2PEQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2
|
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2
|
||||||
react: ^16.8.0 || ^17 || ^18
|
react: ^16.8.0 || ^17 || ^18
|
||||||
|
|
@ -5044,6 +5062,20 @@ packages:
|
||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
'@nodesecure/estree-ast-utils@1.5.0':
|
||||||
|
resolution: {integrity: sha512-uRdPpBQOSvn+iuLWlbtP6NZTI54ALhFZNRc5K+ZaYT8FeYFu0g6HwlNwwwPyyTB1kXyysRRi9j9fkW9bAYSXtw==}
|
||||||
|
|
||||||
|
'@nodesecure/i18n@4.0.1':
|
||||||
|
resolution: {integrity: sha512-i/A8citn5N1i7VBL0PbryAx3zM3sgpFsfVVzIrl5tSTih3cMbQ3QYKCo294U2DMaoncWmI6wO5S71XwctvTHeg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@nodesecure/js-x-ray@8.0.0':
|
||||||
|
resolution: {integrity: sha512-RTDrJfYuLIZ1pnIz+KJOCfH+kHaoxkO0Nyr2xo/6eiuFKzO1F0gSnrE1uQfhUGfeBiMiwx2qzOdyeKFgYn5Baw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@nodesecure/sec-literal@1.2.0':
|
||||||
|
resolution: {integrity: sha512-LGgJmBtnIVHwjZ1QA62YyDvPysdYvGcGn6/JADjY23snTNZS+D9JrkxnChggoNDYj3/GtjutbY/cSlLXEcUJRw==}
|
||||||
|
|
||||||
'@npmcli/agent@2.2.1':
|
'@npmcli/agent@2.2.1':
|
||||||
resolution: {integrity: sha512-H4FrOVtNyWC8MUwL3UfjOsAihHvT1Pe8POj3JvjXhSTJipsZMtgUALCT4mGyYZNxymkUfOw3PUj6dE4QPp6osQ==}
|
resolution: {integrity: sha512-H4FrOVtNyWC8MUwL3UfjOsAihHvT1Pe8POj3JvjXhSTJipsZMtgUALCT4mGyYZNxymkUfOw3PUj6dE4QPp6osQ==}
|
||||||
engines: {node: ^16.14.0 || >=18.0.0}
|
engines: {node: ^16.14.0 || >=18.0.0}
|
||||||
|
|
@ -7787,6 +7819,9 @@ packages:
|
||||||
'@types/connect@3.4.36':
|
'@types/connect@3.4.36':
|
||||||
resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==}
|
resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==}
|
||||||
|
|
||||||
|
'@types/crypto-js@4.2.2':
|
||||||
|
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||||
|
|
||||||
'@types/debug@4.1.7':
|
'@types/debug@4.1.7':
|
||||||
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
|
||||||
|
|
||||||
|
|
@ -9556,6 +9591,10 @@ packages:
|
||||||
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
|
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
default-browser-id@3.0.0:
|
default-browser-id@3.0.0:
|
||||||
resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==}
|
resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -9674,6 +9713,10 @@ packages:
|
||||||
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
|
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
|
digraph-js@2.2.3:
|
||||||
|
resolution: {integrity: sha512-btynrARSW6pBmDz9+cwCxkBJ91CGBxIaNQo7V+ul9/rCRr3HddwehpEMnL6Ru2OeC2pKdRteB1v5TgZRrAAYKQ==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -10483,12 +10526,12 @@ packages:
|
||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
framer-motion@11.15.0:
|
framer-motion@11.11.17:
|
||||||
resolution: {integrity: sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==}
|
resolution: {integrity: sha512-O8QzvoKiuzI5HSAHbcYuL6xU+ZLXbrH7C8Akaato4JzQbX2ULNeniqC2Vo5eiCtFktX9XsJ+7nUhxcl2E2IjpA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@emotion/is-prop-valid': '*'
|
'@emotion/is-prop-valid': '*'
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
react-dom: ^18.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@emotion/is-prop-valid':
|
'@emotion/is-prop-valid':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
@ -10497,6 +10540,9 @@ packages:
|
||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
frequency-set@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Qip6vS0fY/et08sZXumws05weoYvj2ZLkBq3xIwFDFLg8v5IMQiRa+P30tXL0CU6DiYUPLuN3HyRcwW6yWPdeA==}
|
||||||
|
|
||||||
fresh@0.5.2:
|
fresh@0.5.2:
|
||||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
@ -10761,8 +10807,8 @@ packages:
|
||||||
react: ^15.6.0 || ^16.0.0
|
react: ^15.6.0 || ^16.0.0
|
||||||
react-dom: ^15.6.0 || ^16.0.0
|
react-dom: ^15.6.0 || ^16.0.0
|
||||||
|
|
||||||
graphiql@4.0.0-alpha.4:
|
graphiql@4.0.0-alpha.5:
|
||||||
resolution: {integrity: sha512-mMjUJKqNSuHGDBC6JE3CprtBniB3iaGN7Ifisrd6ucmpH7biqA1uIbz1LgzCeQdtndCTHADLJu6dCGOWAcgP9w==}
|
resolution: {integrity: sha512-LAxuJ8kwPlT7YbgM2VMr6bn9xWHRgUL4SRZtvA2VTDDkMgsnTLKd9Vro/EZNKJCTaC7SGOBRImAGCOPlpfTTjw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2
|
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2
|
||||||
react: ^16.8.0 || ^17 || ^18
|
react: ^16.8.0 || ^17 || ^18
|
||||||
|
|
@ -11284,6 +11330,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
|
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-base64@1.1.0:
|
||||||
|
resolution: {integrity: sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-bigint@1.0.4:
|
is-bigint@1.0.4:
|
||||||
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
||||||
|
|
||||||
|
|
@ -11390,6 +11440,9 @@ packages:
|
||||||
is-map@2.0.2:
|
is-map@2.0.2:
|
||||||
resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
|
resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
|
||||||
|
|
||||||
|
is-minified-code@2.0.0:
|
||||||
|
resolution: {integrity: sha512-I1BHmOxm7owypunUWnYx2Ggdhg3lzdyJXLepi8NuR/IsvgVgkwjLj+12iYAGUklu0Xvy3nXGcDSKGbE0Q0Nkag==}
|
||||||
|
|
||||||
is-negative-zero@2.0.2:
|
is-negative-zero@2.0.2:
|
||||||
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
|
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -11492,6 +11545,10 @@ packages:
|
||||||
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
|
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
is-svg@4.4.0:
|
||||||
|
resolution: {integrity: sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
is-symbol@1.0.4:
|
is-symbol@1.0.4:
|
||||||
resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
|
resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -12030,6 +12087,9 @@ packages:
|
||||||
lodash.isarguments@3.1.0:
|
lodash.isarguments@3.1.0:
|
||||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0:
|
||||||
|
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6:
|
lodash.isplainobject@4.0.6:
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
|
||||||
|
|
@ -12054,6 +12114,9 @@ packages:
|
||||||
lodash.startcase@4.4.0:
|
lodash.startcase@4.4.0:
|
||||||
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
|
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
|
||||||
|
|
||||||
|
lodash.uniqwith@4.5.0:
|
||||||
|
resolution: {integrity: sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==}
|
||||||
|
|
||||||
lodash.xorby@4.7.0:
|
lodash.xorby@4.7.0:
|
||||||
resolution: {integrity: sha512-gYiD6nvuQy0AEkMoUju+t4f4Rn18fjsLB/7x7YZFqtFT9kmegRLrj/uGEQVyVDy7otTmSrIMXNOk2wwuLcfHCQ==}
|
resolution: {integrity: sha512-gYiD6nvuQy0AEkMoUju+t4f4Rn18fjsLB/7x7YZFqtFT9kmegRLrj/uGEQVyVDy7otTmSrIMXNOk2wwuLcfHCQ==}
|
||||||
|
|
||||||
|
|
@ -12286,6 +12349,10 @@ packages:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
meriyah@5.0.0:
|
||||||
|
resolution: {integrity: sha512-tNlPDP4AzkH/7cROw7PKJ7mCLe/ZLpa2ja23uqB35vt63+8dgZi2NKLJMrkjxLcxArnLJVvd3Y/7pRl3OLR7yg==}
|
||||||
|
engines: {node: '>=10.4.0'}
|
||||||
|
|
||||||
mermaid@11.2.1:
|
mermaid@11.2.1:
|
||||||
resolution: {integrity: sha512-F8TEaLVVyxTUmvKswVFyOkjPrlJA5h5vNR1f7ZnSWSpqxgEZG1hggtn/QCa7znC28bhlcrNh10qYaIiill7q4A==}
|
resolution: {integrity: sha512-F8TEaLVVyxTUmvKswVFyOkjPrlJA5h5vNR1f7ZnSWSpqxgEZG1hggtn/QCa7znC28bhlcrNh10qYaIiill7q4A==}
|
||||||
|
|
||||||
|
|
@ -12693,12 +12760,6 @@ packages:
|
||||||
moo@0.5.2:
|
moo@0.5.2:
|
||||||
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
|
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
|
||||||
|
|
||||||
motion-dom@11.14.3:
|
|
||||||
resolution: {integrity: sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==}
|
|
||||||
|
|
||||||
motion-utils@11.14.3:
|
|
||||||
resolution: {integrity: sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==}
|
|
||||||
|
|
||||||
mri@1.2.0:
|
mri@1.2.0:
|
||||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
@ -14380,6 +14441,9 @@ packages:
|
||||||
safe-regex2@3.1.0:
|
safe-regex2@3.1.0:
|
||||||
resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==}
|
resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==}
|
||||||
|
|
||||||
|
safe-regex@2.1.1:
|
||||||
|
resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==}
|
||||||
|
|
||||||
safe-stable-stringify@2.4.2:
|
safe-stable-stringify@2.4.2:
|
||||||
resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==}
|
resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -15186,6 +15250,9 @@ packages:
|
||||||
'@swc/wasm':
|
'@swc/wasm':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ts-pattern@5.5.0:
|
||||||
|
resolution: {integrity: sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==}
|
||||||
|
|
||||||
tsconfck@3.0.3:
|
tsconfck@3.0.3:
|
||||||
resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==}
|
resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
|
|
@ -16449,8 +16516,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha256-browser': 3.0.0
|
'@aws-crypto/sha256-browser': 3.0.0
|
||||||
'@aws-crypto/sha256-js': 3.0.0
|
'@aws-crypto/sha256-js': 3.0.0
|
||||||
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
|
'@aws-sdk/client-sso-oidc': 3.596.0
|
||||||
'@aws-sdk/client-sts': 3.596.0
|
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||||
'@aws-sdk/core': 3.592.0
|
'@aws-sdk/core': 3.592.0
|
||||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
|
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
|
||||||
'@aws-sdk/middleware-host-header': 3.577.0
|
'@aws-sdk/middleware-host-header': 3.577.0
|
||||||
|
|
@ -16557,11 +16624,11 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)':
|
'@aws-sdk/client-sso-oidc@3.596.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha256-browser': 3.0.0
|
'@aws-crypto/sha256-browser': 3.0.0
|
||||||
'@aws-crypto/sha256-js': 3.0.0
|
'@aws-crypto/sha256-js': 3.0.0
|
||||||
'@aws-sdk/client-sts': 3.596.0
|
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||||
'@aws-sdk/core': 3.592.0
|
'@aws-sdk/core': 3.592.0
|
||||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
|
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
|
||||||
'@aws-sdk/middleware-host-header': 3.577.0
|
'@aws-sdk/middleware-host-header': 3.577.0
|
||||||
|
|
@ -16600,7 +16667,6 @@ snapshots:
|
||||||
'@smithy/util-utf8': 3.0.0
|
'@smithy/util-utf8': 3.0.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/client-sts'
|
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)':
|
'@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)':
|
||||||
|
|
@ -16734,11 +16800,11 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/client-sts@3.596.0':
|
'@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-crypto/sha256-browser': 3.0.0
|
'@aws-crypto/sha256-browser': 3.0.0
|
||||||
'@aws-crypto/sha256-js': 3.0.0
|
'@aws-crypto/sha256-js': 3.0.0
|
||||||
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
|
'@aws-sdk/client-sso-oidc': 3.596.0
|
||||||
'@aws-sdk/core': 3.592.0
|
'@aws-sdk/core': 3.592.0
|
||||||
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
|
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
|
||||||
'@aws-sdk/middleware-host-header': 3.577.0
|
'@aws-sdk/middleware-host-header': 3.577.0
|
||||||
|
|
@ -16777,6 +16843,7 @@ snapshots:
|
||||||
'@smithy/util-utf8': 3.0.0
|
'@smithy/util-utf8': 3.0.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
- '@aws-sdk/client-sso-oidc'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
'@aws-sdk/client-sts@3.716.0':
|
'@aws-sdk/client-sts@3.716.0':
|
||||||
|
|
@ -16890,7 +16957,7 @@ snapshots:
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)':
|
'@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-sts': 3.596.0
|
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||||
'@aws-sdk/credential-provider-env': 3.587.0
|
'@aws-sdk/credential-provider-env': 3.587.0
|
||||||
'@aws-sdk/credential-provider-http': 3.596.0
|
'@aws-sdk/credential-provider-http': 3.596.0
|
||||||
'@aws-sdk/credential-provider-process': 3.587.0
|
'@aws-sdk/credential-provider-process': 3.587.0
|
||||||
|
|
@ -17009,7 +17076,7 @@ snapshots:
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)':
|
'@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-sts': 3.596.0
|
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
|
||||||
'@aws-sdk/types': 3.577.0
|
'@aws-sdk/types': 3.577.0
|
||||||
'@smithy/property-provider': 3.1.10
|
'@smithy/property-provider': 3.1.10
|
||||||
'@smithy/types': 3.7.1
|
'@smithy/types': 3.7.1
|
||||||
|
|
@ -17184,7 +17251,7 @@ snapshots:
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)':
|
'@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
|
'@aws-sdk/client-sso-oidc': 3.596.0
|
||||||
'@aws-sdk/types': 3.577.0
|
'@aws-sdk/types': 3.577.0
|
||||||
'@smithy/property-provider': 3.1.10
|
'@smithy/property-provider': 3.1.10
|
||||||
'@smithy/shared-ini-file-loader': 3.1.11
|
'@smithy/shared-ini-file-loader': 3.1.11
|
||||||
|
|
@ -18646,7 +18713,7 @@ snapshots:
|
||||||
fastq: 1.17.1
|
fastq: 1.17.1
|
||||||
glob: 10.3.12
|
glob: 10.3.12
|
||||||
|
|
||||||
'@fastify/vite@6.0.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)':
|
'@fastify/vite@6.0.6(patch_hash=wz23vdqq6qtsz64wb433afnvou)(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/middie': 8.3.1
|
'@fastify/middie': 8.3.1
|
||||||
'@fastify/static': 6.12.0
|
'@fastify/static': 6.12.0
|
||||||
|
|
@ -18702,22 +18769,22 @@ snapshots:
|
||||||
graphql: 16.9.0
|
graphql: 16.9.0
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
|
|
||||||
'@graphiql/plugin-explorer@4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@graphiql/plugin-explorer@4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@graphiql/react': 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@graphiql/react': 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
graphiql-explorer: 0.9.0(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
graphiql-explorer: 0.9.0(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
graphql: 16.9.0
|
graphql: 16.9.0
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
'@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@graphiql/toolkit': 0.10.0(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)
|
'@graphiql/toolkit': 0.10.0(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)
|
||||||
'@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-dropdown-menu': 2.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@radix-ui/react-dropdown-menu': 2.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-tooltip': 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@radix-ui/react-tooltip': 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@types/codemirror': 5.60.15
|
'@types/codemirror': 5.60.15
|
||||||
clsx: 1.2.1
|
clsx: 1.2.1
|
||||||
codemirror: 5.65.9
|
codemirror: 5.65.9
|
||||||
|
|
@ -20344,6 +20411,35 @@ snapshots:
|
||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.17.1
|
fastq: 1.17.1
|
||||||
|
|
||||||
|
'@nodesecure/estree-ast-utils@1.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@nodesecure/sec-literal': 1.2.0
|
||||||
|
|
||||||
|
'@nodesecure/i18n@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
cacache: 18.0.2
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
lodash.get: 4.4.2
|
||||||
|
|
||||||
|
'@nodesecure/js-x-ray@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@nodesecure/estree-ast-utils': 1.5.0
|
||||||
|
'@nodesecure/sec-literal': 1.2.0
|
||||||
|
digraph-js: 2.2.3
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
frequency-set: 1.0.2
|
||||||
|
is-minified-code: 2.0.0
|
||||||
|
meriyah: 5.0.0
|
||||||
|
safe-regex: 2.1.1
|
||||||
|
ts-pattern: 5.5.0
|
||||||
|
|
||||||
|
'@nodesecure/sec-literal@1.2.0':
|
||||||
|
dependencies:
|
||||||
|
frequency-set: 1.0.2
|
||||||
|
is-base64: 1.1.0
|
||||||
|
is-svg: 4.4.0
|
||||||
|
string-width: 5.1.2
|
||||||
|
|
||||||
'@npmcli/agent@2.2.1':
|
'@npmcli/agent@2.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.0
|
agent-base: 7.1.0
|
||||||
|
|
@ -23962,6 +24058,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.9.3
|
'@types/node': 22.9.3
|
||||||
|
|
||||||
|
'@types/crypto-js@4.2.2': {}
|
||||||
|
|
||||||
'@types/debug@4.1.7':
|
'@types/debug@4.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 0.7.34
|
'@types/ms': 0.7.34
|
||||||
|
|
@ -26012,6 +26110,8 @@ snapshots:
|
||||||
|
|
||||||
deepmerge@4.2.2: {}
|
deepmerge@4.2.2: {}
|
||||||
|
|
||||||
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
default-browser-id@3.0.0:
|
default-browser-id@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bplist-parser: 0.2.0
|
bplist-parser: 0.2.0
|
||||||
|
|
@ -26099,6 +26199,11 @@ snapshots:
|
||||||
|
|
||||||
diff@5.2.0: {}
|
diff@5.2.0: {}
|
||||||
|
|
||||||
|
digraph-js@2.2.3:
|
||||||
|
dependencies:
|
||||||
|
lodash.isequal: 4.5.0
|
||||||
|
lodash.uniqwith: 4.5.0
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type: 4.0.0
|
path-type: 4.0.0
|
||||||
|
|
@ -27274,16 +27379,16 @@ snapshots:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
framer-motion@11.15.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
framer-motion@11.11.17(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-dom: 11.14.3
|
|
||||||
motion-utils: 11.14.3
|
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@emotion/is-prop-valid': 1.2.2
|
'@emotion/is-prop-valid': 1.2.2
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
|
frequency-set@1.0.2: {}
|
||||||
|
|
||||||
fresh@0.5.2: {}
|
fresh@0.5.2: {}
|
||||||
|
|
||||||
fromentries@1.3.2: {}
|
fromentries@1.3.2: {}
|
||||||
|
|
@ -27606,9 +27711,9 @@ snapshots:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
graphiql@4.0.0-alpha.4(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
graphiql@4.0.0-alpha.5(patch_hash=yjzkcog7ut7wshk4npre67txki)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@graphiql/react': 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@graphiql/react': 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
graphql: 16.9.0
|
graphql: 16.9.0
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
@ -28328,6 +28433,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag: 1.0.0
|
has-tostringtag: 1.0.0
|
||||||
|
|
||||||
|
is-base64@1.1.0: {}
|
||||||
|
|
||||||
is-bigint@1.0.4:
|
is-bigint@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-bigints: 1.0.2
|
has-bigints: 1.0.2
|
||||||
|
|
@ -28412,6 +28519,8 @@ snapshots:
|
||||||
|
|
||||||
is-map@2.0.2: {}
|
is-map@2.0.2: {}
|
||||||
|
|
||||||
|
is-minified-code@2.0.0: {}
|
||||||
|
|
||||||
is-negative-zero@2.0.2: {}
|
is-negative-zero@2.0.2: {}
|
||||||
|
|
||||||
is-network-error@1.0.0: {}
|
is-network-error@1.0.0: {}
|
||||||
|
|
@ -28485,6 +28594,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
better-path-resolve: 1.0.0
|
better-path-resolve: 1.0.0
|
||||||
|
|
||||||
|
is-svg@4.4.0:
|
||||||
|
dependencies:
|
||||||
|
fast-xml-parser: 4.4.1
|
||||||
|
|
||||||
is-symbol@1.0.4:
|
is-symbol@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.0.3
|
has-symbols: 1.0.3
|
||||||
|
|
@ -29004,6 +29117,8 @@ snapshots:
|
||||||
|
|
||||||
lodash.isarguments@3.1.0: {}
|
lodash.isarguments@3.1.0: {}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0: {}
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6: {}
|
lodash.isplainobject@4.0.6: {}
|
||||||
|
|
||||||
lodash.lowercase@4.3.0: {}
|
lodash.lowercase@4.3.0: {}
|
||||||
|
|
@ -29020,6 +29135,8 @@ snapshots:
|
||||||
|
|
||||||
lodash.startcase@4.4.0: {}
|
lodash.startcase@4.4.0: {}
|
||||||
|
|
||||||
|
lodash.uniqwith@4.5.0: {}
|
||||||
|
|
||||||
lodash.xorby@4.7.0: {}
|
lodash.xorby@4.7.0: {}
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
@ -29403,6 +29520,8 @@ snapshots:
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
|
meriyah@5.0.0: {}
|
||||||
|
|
||||||
mermaid@11.2.1:
|
mermaid@11.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@braintree/sanitize-url': 7.1.0
|
'@braintree/sanitize-url': 7.1.0
|
||||||
|
|
@ -30176,10 +30295,6 @@ snapshots:
|
||||||
|
|
||||||
moo@0.5.2: {}
|
moo@0.5.2: {}
|
||||||
|
|
||||||
motion-dom@11.14.3: {}
|
|
||||||
|
|
||||||
motion-utils@11.14.3: {}
|
|
||||||
|
|
||||||
mri@1.2.0: {}
|
mri@1.2.0: {}
|
||||||
|
|
||||||
mrmime@2.0.0: {}
|
mrmime@2.0.0: {}
|
||||||
|
|
@ -32068,6 +32183,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
ret: 0.4.3
|
ret: 0.4.3
|
||||||
|
|
||||||
|
safe-regex@2.1.1:
|
||||||
|
dependencies:
|
||||||
|
regexp-tree: 0.1.27
|
||||||
|
|
||||||
safe-stable-stringify@2.4.2: {}
|
safe-stable-stringify@2.4.2: {}
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
@ -32971,6 +33090,8 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@swc/core': 1.10.1(@swc/helpers@0.5.11)
|
'@swc/core': 1.10.1(@swc/helpers@0.5.11)
|
||||||
|
|
||||||
|
ts-pattern@5.5.0: {}
|
||||||
|
|
||||||
tsconfck@3.0.3(typescript@5.7.2):
|
tsconfck@3.0.3(typescript@5.7.2):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue