UNPKG

@graphql-mesh/fusion-runtime

Version:

Runtime for GraphQL Mesh Fusion Supergraph

308 lines (307 loc) • 13.3 kB
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(); }, }; }