Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
},
"workspaces": [
"packages/explorer",
"packages/sandbox"
"packages/sandbox",
"packages/embed-helpers"
],
"devDependencies": {
"@babel/core": "^7.18.5",
Expand Down
38 changes: 38 additions & 0 deletions packages/embed-helpers/createRollupConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import typescript from '@rollup/plugin-typescript';
import babel from '@rollup/plugin-babel';
import json from '@rollup/plugin-json';

export function createRollupConfig(options) {
return {
input: {
index: 'src/index.ts',
},
output: {
format: options.format,
freeze: false,
esModule: true,
name: 'embed-helpers',
exports: 'named',
sourcemap: false,
dir: `./dist`,
entryFileNames:
// All of our esm files have .mjs extensions
options.format === 'esm' ? '[name].mjs' : '[name].production.min.cjs',
...(options.format === 'cjs' && {
chunkFileNames: '[name].production.min.cjs',
}),
},
external: ['use-deep-compare-effect', 'react'],
plugins: [
typescript({
tsconfig: './tsconfig.json',
}),
babel({
exclude: 'node_modules/**',
extensions: ['.js', '.jsx', '.ts', '.tsx'],
babelHelpers: 'bundled',
}),
json(),
],
};
}
62 changes: 62 additions & 0 deletions packages/embed-helpers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "@apollo/embed-helpers",
"version": "0.0.0",
"author": "[email protected]",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/apollographql/embeddable-explorer"
},
"homepage": "https://github.com/apollographql/embeddable-explorer/embed-helpers#readme",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"typings": "dist/src/index.d.ts",
"engines": {
"node": ">=12.0",
"npm": ">=7.0"
},
"volta": {
"node": "16.13.0",
"npm": "8.3.1"
},
"scripts": {
"build": "npm run build:cjs-esm",
"build:cjs-esm": "rm -rf dist && rollup -c rollup.cjs-esm.config.js && cp src/index.cjs dist/index.cjs",
"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
"size": "size-limit",
"analyze": "size-limit --why",
"typescript:check": "tsc --noEmit",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write ."
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"endOfLine": "auto"
},
"size-limit": [
{
"path": "dist/index.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/index.mjs",
"limit": "10 KB"
}
],
"dependencies": {
"@types/whatwg-mimetype": "^3.0.0",
"graphql-ws": "^5.9.0",
"subscriptions-transport-ws": "^0.11.0",
"whatwg-mimetype": "^3.0.0",
"zen-observable-ts": "^1.1.0"
}
}

10 changes: 10 additions & 0 deletions packages/embed-helpers/rollup.cjs-esm.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createRollupConfig } from './createRollupConfig';

export default [
createRollupConfig({
format: 'cjs',
}),
createRollupConfig({
format: 'esm',
}),
];
43 changes: 43 additions & 0 deletions packages/embed-helpers/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const EMBEDDABLE_SANDBOX_URL = (__testLocal__ = false) =>
__testLocal__
? 'https://embed.apollo.local:3000/sandbox/explorer'
: 'https://sandbox.embed.apollographql.com/sandbox/explorer';

export const EMBEDDABLE_EXPLORER_URL = (__testLocal__ = false) =>
__testLocal__
? 'https://embed.apollo.local:3000'
: 'https://explorer.embed.apollographql.com';

// Message types for Explorer state
export const EXPLORER_LISTENING_FOR_SCHEMA = 'ExplorerListeningForSchema';
export const EXPLORER_LISTENING_FOR_STATE = 'ExplorerListeningForState';
export const SET_OPERATION = 'SetOperation';
export const SCHEMA_ERROR = 'SchemaError';
export const SCHEMA_RESPONSE = 'SchemaResponse';

// Message types for queries and mutations
export const EXPLORER_QUERY_MUTATION_REQUEST = 'ExplorerRequest';
export const EXPLORER_QUERY_MUTATION_RESPONSE = 'ExplorerResponse';
export const TRACE_KEY = 'ftv1';

// Message types for subscriptions
export const EXPLORER_SUBSCRIPTION_REQUEST = 'ExplorerSubscriptionRequest';
export const EXPLORER_SUBSCRIPTION_RESPONSE = 'ExplorerSubscriptionResponse';
export const EXPLORER_SUBSCRIPTION_TERMINATION =
'ExplorerSubscriptionTermination';
export const EXPLORER_SET_SOCKET_ERROR = 'ExplorerSetSocketError';
export const EXPLORER_SET_SOCKET_STATUS = 'ExplorerSetSocketStatus';
export const IFRAME_DOM_ID = (uniqueId: number) => `apollo-embed-${uniqueId}`;

// Message types for authentication
export const EXPLORER_LISTENING_FOR_HANDSHAKE = 'ExplorerListeningForHandshake';
export const HANDSHAKE_RESPONSE = 'HandshakeResponse';
export const SET_PARTIAL_AUTHENTICATION_TOKEN_FOR_PARENT =
'SetPartialAuthenticationTokenForParent';
export const TRIGGER_LOGOUT_IN_PARENT = 'TriggerLogoutInParent';
export const EXPLORER_LISTENING_FOR_PARTIAL_TOKEN =
'ExplorerListeningForPartialToken';
export const PARTIAL_AUTHENTICATION_TOKEN_RESPONSE =
'PartialAuthenticationTokenResponse';
export const INTROSPECTION_QUERY_WITH_HEADERS = 'IntrospectionQueryWithHeaders';
export const PARENT_LOGOUT_SUCCESS = 'ParentLogoutSuccess';
102 changes: 102 additions & 0 deletions packages/embed-helpers/src/constructMultipartForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { JSONValue } from './types';

export type FileVariable = {
variableKey: string;
files: { arrayBuffer: ArrayBuffer; fileName: string }[];
isMultiFile: boolean;
};

// https://github.com/apollographql/apollo-client/blob/cbcf951256b22553bdb065dfa0d32c0a4ca804d3/src/link/http/serializeFetchParameter.ts
export const serializeFetchParameter = (p: any, label: string) => {
let serialized;
try {
serialized = JSON.stringify(p);
} catch (e) {
const parseError = new Error(
`Network request failed. ${label} is not serializable`
);
throw parseError;
}
return serialized;
};

export const constructMultipartForm = async ({
fileVariables: inputtedFileVariables,
requestBody,
}: {
fileVariables: FileVariable[];
requestBody: {
operationName?: string;
query: string;
variables?: Record<string, string>;
};
}) => {
const fileVariables: {
variableKey: string;
files: File[];
isMultiFile: boolean;
}[] = inputtedFileVariables.map((fileVariable) => ({
...fileVariable,
files: fileVariable.files.map(
({ arrayBuffer, fileName }) =>
new File([new Blob([arrayBuffer])], fileName)
),
}));

// the map element of a FormData maps indices to a single item array of variable names
// as seen here https://github.com/jaydenseric/graphql-multipart-request-spec#file-list
const map: Record<number, string[]> = {};
let i = 0;
// map must be the first thing in the form, followed by the files
// other wise you get the error:
// Misordered multipart fields; files should follow ‘map’ (https://github.com/jaydenseric/graphql-multipart-request-spec).
const filesToAppend: [string, File, string][] = [];
// variables are added to the operation body with null values, the variable
// name is used to match them to files uploaded in the later part of the request
// according to the spec https://github.com/jaydenseric/graphql-multipart-request-spec
let variablesWithNullsForFiles:
| Record<string, JSONValue | undefined | null | null[]>
| undefined = requestBody.variables;
fileVariables.forEach(
({ files, variableKey, isMultiFile }, fileVariableIndex) => {
if (files?.length) {
variablesWithNullsForFiles = {
...variablesWithNullsForFiles,
[variableKey]: isMultiFile
? new Array(files.length).fill(null)
: null,
};
Array.from(files).forEach((file) => {
map[i] = [
`${
fileVariables.length > 1 ? `${fileVariableIndex}.` : ''
}variables.${variableKey}${isMultiFile ? `.${i}` : ''}`,
];
// in the request, there is expected to be a number appended that corresponds to each file
// https://github.com/jaydenseric/graphql-multipart-request-spec#file-list
filesToAppend.push([i.toString(), file, file.name]);
i++;
});
}
}
);
const form = new FormData();
form.append(
'operations',
serializeFetchParameter(
{
query: requestBody.query,
operationName: requestBody.operationName,
variables: variablesWithNullsForFiles,
},
'Payload'
)
);

form.append('map', JSON.stringify(map));
filesToAppend.forEach((item) => {
form.append(item[0], item[1], item[2]);
});

return form;
};
20 changes: 20 additions & 0 deletions packages/embed-helpers/src/defaultHandleRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { HandleRequest } from './postMessageRelayHelpers';

export const defaultHandleRequest = ({
legacyIncludeCookies,
}: {
legacyIncludeCookies?: boolean;
}): HandleRequest => {
const handleRequestWithCookiePref: HandleRequest = (endpointUrl, options) =>
fetch(endpointUrl, {
...options,
...(legacyIncludeCookies
? { credentials: 'include' }
: // if the user doesn't pass this value then we should use the credentials option sent from the
// studio postMessage request. otherwise this would overwrite it.
legacyIncludeCookies !== undefined
? { credentials: 'omit' }
: {}),
});
return handleRequestWithCookiePref;
};
2 changes: 2 additions & 0 deletions packages/embed-helpers/src/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
module.exports = require('./index.production.min.cjs');
Loading