UNPKG

@graphql-mesh/fusion-execution

Version:
196 lines (195 loc) 8.55 kB
import jsonpatch from 'fast-json-patch'; import { Kind, valueFromASTUntyped, } from 'graphql'; import { getRootTypeMap, isAsyncIterable, isPromise, mapAsyncIterator } from '@graphql-tools/utils'; import { Repeater } from '@repeaterjs/repeater'; import { createExecutableResolverOperationNodesWithDependencyMap, executeResolverOperationNodesWithDependenciesInParallel, } from './execution.js'; import { flattenSelections } from './flattenSelections.js'; import { visitFieldNodeForTypeResolvers } from './query-planning.js'; import { serializeExecutableResolverOperationNode, } from './serialization.js'; export function planOperation(fusiongraph, document, operationName) { let operationAst; const fragments = Object.create(null); for (const definition of document.definitions) { if (definition.kind === 'OperationDefinition') { if (!operationName && operationAst) { throw new Error('Must provide operation name if query contains multiple operations'); } if (!operationName || definition.name?.value === operationName) { operationAst = definition; } } else if (definition.kind === 'FragmentDefinition') { fragments[definition.name.value] = definition; } } if (!operationAst) { throw new Error(`No operation found with name ${operationName}`); } const defaultVariables = Object.create(null); operationAst.variableDefinitions?.forEach(variableDefinition => { if (variableDefinition.defaultValue) { defaultVariables[variableDefinition.variable.name.value] = valueFromASTUntyped(variableDefinition.defaultValue, defaultVariables); } }); const flattenedFakeFieldNode = { kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__fake', }, arguments: operationAst.variableDefinitions?.map(variableDefinition => ({ kind: Kind.ARGUMENT, name: variableDefinition.variable.name, value: { kind: Kind.VARIABLE, name: variableDefinition.variable.name, }, })), selectionSet: { kind: Kind.SELECTION_SET, selections: flattenSelections(operationAst.selectionSet.selections, fragments), }, }; const rootTypeMap = getRootTypeMap(fusiongraph); const operationType = operationAst.operation; const rootType = rootTypeMap.get(operationType); if (!rootType) { throw new Error(`No root type found for operation type ${operationType}`); } const rootVariableMap = new Map(); for (const variableDefinition of operationAst.variableDefinitions || []) { rootVariableMap.set(variableDefinition.variable.name.value, variableDefinition); } const planForFakeFieldNode = visitFieldNodeForTypeResolvers('ROOT', flattenedFakeFieldNode, rootType, fusiongraph, { currentVariableIndex: 0, rootVariableMap, }); return { resolverOperationNodes: planForFakeFieldNode.resolverOperationNodes, resolverDependencyFieldMap: planForFakeFieldNode.resolverDependencyFieldMap, defaultVariables, }; } export function executeOperation({ fusiongraph, onExecute, document, operationName, variables = {}, context = {}, }) { const executablePlan = createExecutablePlanForOperation({ fusiongraph, document, operationName }); return executeOperationPlan({ executablePlan, onExecute, variables, context }); } export function executeOperationWithPatches({ fusiongraph, onExecute, document, operationName, variables = {}, context = {}, }) { const executablePlan = createExecutablePlanForOperation({ fusiongraph, document, operationName }); return executeOperationPlanWithPatches({ executablePlan, onExecute, variables, context }); } export function createExecutablePlanForOperation({ fusiongraph, document, operationName, }) { const plan = planOperation(fusiongraph, document, operationName); const executablePlan = createExecutableResolverOperationNodesWithDependencyMap(plan.resolverOperationNodes, plan.resolverDependencyFieldMap, 0); return { resolverOperationNodes: executablePlan.resolverOperationNodes, resolverDependencyFieldMap: executablePlan.resolverDependencyFieldMap, defaultVariables: plan.defaultVariables, }; } function removeInternalFieldsFromResponse(response) { if (Array.isArray(response)) { return response.map(removeInternalFieldsFromResponse); } else if (typeof response === 'object' && response != null && (!response.constructor || response.constructor.name === 'Object')) { return Object.fromEntries(Object.entries(response) .filter(([key]) => !key.startsWith('__variable')) .map(([key, value]) => [key, removeInternalFieldsFromResponse(value)])); } else { return response; } } function prepareExecutionResult(planExecutionResult, errors, executionPlan) { return { data: planExecutionResult?.exported != null ? removeInternalFieldsFromResponse(planExecutionResult.exported) : undefined, errors: errors.length > 0 ? errors : undefined, extensions: globalThis.process?.env?.DEBUG ? { executionPlan: serializeExecutableOperationPlan(executionPlan), outputVariables: Object.fromEntries(planExecutionResult?.outputVariableMap || []), } : undefined, }; } export function executeOperationPlanWithPatches(params) { return new Repeater(async function (push, stop) { try { const resMaybeAsyncIter = await executeOperationPlan(params); if (isAsyncIterable(resMaybeAsyncIter)) { let lastRes; for await (const res of resMaybeAsyncIter) { if (!lastRes) { await push({ ...res, hasNext: true, }); } else { const patches = jsonpatch.compare(lastRes, res); for (const patch of patches) { if (patch.op === 'add' || patch.op === 'replace') { const [, ...pathForGraphQL] = patch.path.split('/'); await push({ ...res, path: pathForGraphQL, data: patch.value, hasNext: true, }); } } } lastRes = res; } await push({ hasNext: false, }); } else { await push({ ...resMaybeAsyncIter, hasNext: false, }); } await stop(); } catch (e) { await stop(e); } }); } export function executeOperationPlan({ executablePlan, onExecute, variables = {}, context, }) { const errors = []; const res$ = executeResolverOperationNodesWithDependenciesInParallel({ context, resolverOperationNodes: executablePlan.resolverOperationNodes, fieldDependencyMap: executablePlan.resolverDependencyFieldMap, inputVariableMap: new Map([ ...Object.entries(executablePlan.defaultVariables), ...Object.entries(variables), ]), onExecute, path: [], errors, }); if (isPromise(res$)) { return res$.then(res => prepareExecutionResult(res, errors, executablePlan)); } if (isAsyncIterable(res$)) { return mapAsyncIterator(res$, res => prepareExecutionResult(res, errors, executablePlan)); } return prepareExecutionResult(res$, errors, executablePlan); } export function serializeExecutableOperationPlan(executablePlan) { return { resolverOperationNodes: executablePlan.resolverOperationNodes.map(node => serializeExecutableResolverOperationNode(node)), resolverDependencyFieldMap: Object.fromEntries([...executablePlan.resolverDependencyFieldMap.entries()].map(([key, value]) => [ key, value.map(serializeExecutableResolverOperationNode), ])), }; }