@graphql-mesh/fusion-runtime
Version:
Runtime for GraphQL Mesh Fusion Supergraph
316 lines (315 loc) • 14.4 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';
import { iterateAsync, mapMaybePromise } from '@graphql-mesh/utils';
import { stitchSchemas } from '@graphql-tools/stitch';
import { getDirective, isAsyncIterable, 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);
return mapMaybePromise(transport$, transport => transport.getSubgraphExecutor(transportContext));
}
export function getExecutorForFusiongraph({ fusiongraph: fusiongraphInput, transports = defaultTransportsOption, plugins, ...transportBaseContext }) {
const fusiongraph = ensureSchema(fusiongraphInput);
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,
transportEntry,
executionRequest: subgraphExecReq,
setExecutionRequest(newExecutionRequest) {
subgraphExecReq = newExecutionRequest;
},
executor: currentExecutor,
setExecutor(newExecutor) {
currentExecutor = newExecutor;
},
}), onSubgraphExecuteDoneHooks);
function handleOnSubgraphExecuteHooksResult() {
if (onSubgraphExecuteDoneHooks.length) {
// eslint-disable-next-line no-inner-declarations
function handleExecutorResWithHooks(currentResult) {
const executeDoneResults = [];
const onSubgraphExecuteDoneHooksRes$ = iterateAsync(onSubgraphExecuteDoneHooks, onSubgraphExecuteDoneHook => onSubgraphExecuteDoneHook({
result: currentResult,
setResult(newResult) {
currentResult = newResult;
},
}), executeDoneResults);
function handleExecuteDoneResults(result) {
if (!isAsyncIterable(result)) {
return result;
}
if (executeDoneResults.length === 0) {
return result;
}
const onNextHooks = [];
const onEndHooks = [];
for (const executeDoneResult of executeDoneResults) {
if (executeDoneResult.onNext) {
onNextHooks.push(executeDoneResult.onNext);
}
if (executeDoneResult.onEnd) {
onEndHooks.push(executeDoneResult.onEnd);
}
}
return mapAsyncIterator(result[Symbol.asyncIterator](), currentResult => {
if (onNextHooks.length === 0) {
return currentResult;
}
const $ = iterateAsync(onNextHooks, onNext => onNext({
result: currentResult,
setResult: res => {
currentResult = res;
},
}));
return mapMaybePromise($, () => currentResult);
}, undefined, () => onEndHooks.length === 0
? undefined
: iterateAsync(onEndHooks, onEnd => onEnd()));
}
return mapMaybePromise(onSubgraphExecuteDoneHooksRes$, () => handleExecuteDoneResults(currentResult));
}
const executorRes$ = currentExecutor(subgraphExecReq);
return mapMaybePromise(executorRes$, handleExecutorResWithHooks);
}
return currentExecutor(subgraphExecReq);
}
return mapMaybePromise(onSubgraphExecuteHooksRes$, 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 });
return mapMaybePromise(executor$, executor_ => {
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);
return mapMaybePromise(fusiongraph$, handleLoadedFusiongraph);
}
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();
},
};
}