@graphql-mesh/fusion-runtime
Version:
Runtime for GraphQL Mesh Fusion Supergraph
308 lines (307 loc) • 13.3 kB
JavaScript
import { buildASTSchema, buildSchema, introspectionFromSchema, isSchema, valueFromASTUntyped, } from 'graphql';
import { createExecutablePlanForOperation, executeOperationPlan, extractSubgraphFromFusiongraph, } from '@graphql-mesh/fusion-execution';
// eslint-disable-next-line import/no-extraneous-dependencies
import { getInContextSDK } from '@graphql-mesh/runtime';
// eslint-disable-next-line import/no-extraneous-dependencies
import { iterateAsync } from '@graphql-mesh/utils';
import { stitchSchemas } from '@graphql-tools/stitch';
import { getDirective, isAsyncIterable, isPromise, mapAsyncIterator, memoize2of4, } from '@graphql-tools/utils';
function getTransportDirectives(fusiongraph) {
const transportDirectives = getDirective(fusiongraph, fusiongraph, 'transport');
if (transportDirectives?.length) {
return transportDirectives;
}
const astNode = fusiongraph.astNode;
if (astNode?.directives?.length) {
return astNode.directives
.filter(directive => directive.name.value === 'transport')
.map(transportDirective => Object.fromEntries(transportDirective.arguments?.map(argument => [
argument.name.value,
valueFromASTUntyped(argument.value),
])));
}
return [];
}
export function getSubgraphTransportMapFromFusiongraph(fusiongraph) {
const subgraphTransportEntryMap = {};
const transportDirectives = getTransportDirectives(fusiongraph);
for (const { kind, subgraph, location, headers, ...options } of transportDirectives) {
subgraphTransportEntryMap[subgraph] = {
kind,
location,
headers,
options,
subgraph,
};
}
return subgraphTransportEntryMap;
}
export const getMemoizedExecutionPlanForOperation = memoize2of4(function getMemoizedExecutionPlanForOperation(fusiongraph, document, operationName, _random) {
return createExecutablePlanForOperation({
fusiongraph,
document,
operationName,
});
});
export function defaultTransportsOption(transportKind) {
return import(`@graphql-mesh/transport-${transportKind}`).catch(err => {
console.error(err);
throw new Error(`No transport found for ${transportKind}. Please install @graphql-mesh/transport-${transportKind}`);
});
}
export function createTransportGetter(transports) {
if (typeof transports === 'function') {
return transports;
}
return function getTransport(transportKind) {
const transport = transports[transportKind];
if (!transport) {
throw new Error(`No transport found for ${transportKind}`);
}
return transport;
};
}
export function getTransportExecutor(transportGetter, transportContext) {
transportContext.logger?.info(`Loading transport ${transportContext.transportEntry?.kind}`);
const transport$ = transportGetter(transportContext.transportEntry?.kind);
if (isPromise(transport$)) {
return transport$.then(transport => transport.getSubgraphExecutor(transportContext));
}
return transport$.getSubgraphExecutor(transportContext);
}
export function getExecutorForFusiongraph({ fusiongraph, transports = defaultTransportsOption, plugins, ...transportBaseContext }) {
const onSubgraphExecuteHooks = [];
if (plugins) {
for (const plugin of plugins) {
if (plugin.onSubgraphExecute) {
onSubgraphExecuteHooks.push(plugin.onSubgraphExecute);
}
}
}
const transportEntryMap = getSubgraphTransportMapFromFusiongraph(fusiongraph);
const subgraphExecutorMap = {};
const transportGetter = createTransportGetter(transports);
function onSubgraphExecute(subgraphName, document, variables, context) {
let executor = subgraphExecutorMap[subgraphName];
if (executor == null) {
transportBaseContext?.logger?.info(`Initializing executor for subgraph ${subgraphName}`);
const transportEntry = transportEntryMap[subgraphName];
// eslint-disable-next-line no-inner-declarations
function wrapExecutorWithHooks(currentExecutor) {
if (onSubgraphExecuteHooks.length) {
return function executorWithHooks(subgraphExecReq) {
const onSubgraphExecuteDoneHooks = [];
const onSubgraphExecuteHooksRes$ = iterateAsync(onSubgraphExecuteHooks, onSubgraphExecuteHook => onSubgraphExecuteHook({
fusiongraph,
subgraphName,
transportKind: transportEntry?.kind,
transportLocation: transportEntry?.location,
transportHeaders: transportEntry?.headers,
transportOptions: transportEntry?.options,
executionRequest: subgraphExecReq,
executor: currentExecutor,
setExecutor(newExecutor) {
currentExecutor = newExecutor;
},
}), onSubgraphExecuteDoneHooks);
function handleOnSubgraphExecuteHooksResult() {
if (onSubgraphExecuteDoneHooks.length) {
// eslint-disable-next-line no-inner-declarations
function handleExecutorResWithHooks(currentResult) {
const onSubgraphExecuteDoneHooksRes$ = iterateAsync(onSubgraphExecuteDoneHooks, onSubgraphExecuteDoneHook => onSubgraphExecuteDoneHook({
result: currentResult,
setResult(newResult) {
currentResult = newResult;
},
}));
if (isPromise(onSubgraphExecuteDoneHooksRes$)) {
return onSubgraphExecuteDoneHooksRes$.then(() => currentResult);
}
return currentResult;
}
const executorRes$ = currentExecutor(subgraphExecReq);
if (isPromise(executorRes$)) {
return executorRes$.then(handleExecutorResWithHooks);
}
if (isAsyncIterable(executorRes$)) {
return mapAsyncIterator(executorRes$, handleExecutorResWithHooks);
}
return handleExecutorResWithHooks(executorRes$);
}
return currentExecutor(subgraphExecReq);
}
if (isPromise(onSubgraphExecuteHooksRes$)) {
return onSubgraphExecuteHooksRes$.then(handleOnSubgraphExecuteHooksResult);
}
return handleOnSubgraphExecuteHooksResult();
};
}
return currentExecutor;
}
executor = function lazyExecutor(subgraphExecReq) {
function getSubgraph() {
return extractSubgraphFromFusiongraph(subgraphName, fusiongraph);
}
const executor$ = getTransportExecutor(transportGetter, transportBaseContext
? {
...transportBaseContext,
subgraphName,
getSubgraph,
transportEntry,
}
: { getSubgraph, transportEntry, subgraphName });
if (isPromise(executor$)) {
return executor$.then(executor_ => {
executor = wrapExecutorWithHooks(executor_);
subgraphExecutorMap[subgraphName] = executor;
return executor(subgraphExecReq);
});
}
executor = wrapExecutorWithHooks(executor$);
subgraphExecutorMap[subgraphName] = executor;
return executor(subgraphExecReq);
};
}
return executor({ document, variables, context });
}
function fusiongraphExecutor(execReq) {
if (execReq.operationName === 'IntrospectionQuery') {
return {
data: introspectionFromSchema(fusiongraph),
};
}
const executablePlan = getMemoizedExecutionPlanForOperation(fusiongraph, execReq.document, execReq.operationName);
return executeOperationPlan({
executablePlan,
onExecute: onSubgraphExecute,
variables: execReq.variables,
context: execReq.context,
});
}
return {
fusiongraphExecutor,
transportEntryMap,
onSubgraphExecute,
};
}
function ensureSchema(source) {
if (isSchema(source)) {
return source;
}
if (typeof source === 'string') {
return buildSchema(source, { noLocation: true, assumeValidSDL: true, assumeValid: true });
}
return buildASTSchema(source, { assumeValidSDL: true, assumeValid: true });
}
function getExecuteFnFromExecutor(executor) {
return function executeFnFromExecutor({ document, variableValues, contextValue, rootValue, operationName, }) {
return executor({
document,
variables: variableValues,
context: contextValue,
operationName,
rootValue,
});
};
}
export function useFusiongraph({ getFusiongraph, transports, additionalResolvers, polling, transportBaseContext, }) {
let fusiongraph;
let lastLoadedFusiongraph;
let executeFn;
let executor;
let yoga;
// TODO: We need to figure this out in a better way
let inContextSDK;
function handleLoadedFusiongraph(loadedFusiongraph) {
// If the fusiongraph is the same, we don't need to do anything
if (lastLoadedFusiongraph != null && lastLoadedFusiongraph === loadedFusiongraph) {
return;
}
lastLoadedFusiongraph = loadedFusiongraph;
fusiongraph = ensureSchema(loadedFusiongraph);
const { fusiongraphExecutor, onSubgraphExecute, transportEntryMap } = getExecutorForFusiongraph({
fusiongraph,
transports,
plugins: yoga.getEnveloped._plugins,
...transportBaseContext,
});
executor = fusiongraphExecutor;
if (additionalResolvers != null) {
fusiongraph = stitchSchemas({
subschemas: [
{
schema: fusiongraph,
executor,
},
],
resolvers: additionalResolvers,
});
const subgraphsForInContextSdk = [];
for (const subgraphName in transportEntryMap) {
subgraphsForInContextSdk.push({
name: subgraphName,
schema: extractSubgraphFromFusiongraph(subgraphName, fusiongraph),
executor(execReq) {
return onSubgraphExecute(subgraphName, execReq.document, execReq.variables, execReq.context);
},
});
}
inContextSDK = getInContextSDK(fusiongraph, subgraphsForInContextSdk, transportBaseContext.logger, []);
}
else {
executeFn = getExecuteFnFromExecutor(executor);
}
}
function getAndSetFusiongraph() {
const fusiongraph$ = getFusiongraph(transportBaseContext);
if (isPromise(fusiongraph$)) {
return fusiongraph$.then(handleLoadedFusiongraph);
}
else {
return handleLoadedFusiongraph(fusiongraph$);
}
}
if (polling) {
setInterval(getAndSetFusiongraph, polling);
}
let initialFusiongraph$;
let initiated = false;
return {
onYogaInit(payload) {
yoga = payload.yoga;
},
onRequestParse() {
return {
onRequestParseDone() {
if (!initiated) {
initialFusiongraph$ = getAndSetFusiongraph();
}
initiated = true;
return initialFusiongraph$;
},
};
},
onEnveloped({ setSchema }) {
setSchema(fusiongraph);
},
onContextBuilding({ extendContext }) {
if (inContextSDK) {
extendContext(inContextSDK);
}
extendContext(transportBaseContext);
},
onExecute({ setExecuteFn }) {
if (executeFn) {
setExecuteFn(executeFn);
}
},
onSubscribe({ setSubscribeFn }) {
if (executeFn) {
setSubscribeFn(executeFn);
}
},
invalidateUnifiedGraph() {
return getAndSetFusiongraph();
},
};
}