UNPKG

@graphql-mesh/fusion-execution

Version:
549 lines (548 loc) 24.2 kB
/* eslint-disable no-inner-declarations */ import { Kind, visit } from 'graphql'; import _ from 'lodash'; import { createGraphQLError, isAsyncIterable, isPromise, mapAsyncIterator, relocatedError, } from '@graphql-tools/utils'; import { Repeater } from '@repeaterjs/repeater'; import { visitResolutionPath } from './visitResolutionPath.js'; function deserializeGraphQLError(error) { if (error.name === 'GraphQLError') { return error; } return createGraphQLError(error.message, { nodes: error.nodes, source: error.source, positions: error.positions, path: error.path, originalError: error.originalError, extensions: error.extensions, }); } export function createExecutableResolverOperationNode(resolverOperationNode, currentId) { const id = currentId++; const providedVariablePathMap = new Map(); const exportPath = []; visitResolutionPath(resolverOperationNode.resolverOperationDocument, ({ path }) => { const lastElem = path[path.length - 1]; if (lastElem === '__export') { exportPath.splice(0, exportPath.length, ...path); } else if (lastElem.startsWith('__variable')) { providedVariablePathMap.set(lastElem, path); } }); // Remove __export from variable paths for (const [, providedVariablePath] of providedVariablePathMap) { const index = providedVariablePath.indexOf('__export'); if (index !== -1) { providedVariablePath.splice(index, 1); } } const newDependencyMap = new Map(); const newBatchedDependencyMap = new Map(); for (const [key, nodes] of resolverOperationNode.resolverDependencyFieldMap) { const batchedNodes = []; const nonBatchedNodes = []; for (const node of nodes) { const executableNode = createExecutableResolverOperationNode(node, currentId++); if (node.batch) { batchedNodes.push(executableNode); } else { nonBatchedNodes.push(executableNode); } } newBatchedDependencyMap.set(key, batchedNodes); newDependencyMap.set(key, nonBatchedNodes); } const requiredVariableNames = new Set(); visit(resolverOperationNode.resolverOperationDocument, { [Kind.VARIABLE_DEFINITION]: node => { requiredVariableNames.add(node.variable.name.value); }, }); const batchedResolverDependencies = []; const resolverDependencies = []; for (const node of resolverOperationNode.resolverDependencies) { const executableNode = createExecutableResolverOperationNode(node, currentId++); if (node.batch) { batchedResolverDependencies.push(executableNode); } else { resolverDependencies.push(executableNode); } } const batchedPreResolverDependencies = []; const resolverPreDependencies = []; for (const node of resolverOperationNode.resolverPreDependencies) { const executableNode = createExecutableResolverOperationNode(node, currentId++); if (node.batch) { batchedPreResolverDependencies.push(executableNode); } else { resolverPreDependencies.push(executableNode); } } return { id, ...resolverOperationNode, resolverDependencies, batchedResolverDependencies, resolverPreDependencies, batchedPreResolverDependencies, resolverDependencyFieldMap: newDependencyMap, batchedResolverDependencyFieldMap: newBatchedDependencyMap, providedVariablePathMap, requiredVariableNames, exportPath, }; } export function createExecutableResolverOperationNodesWithDependencyMap(resolverOperationNodes, resolverDependencyFieldMap, currentId) { const newResolverOperationNodes = resolverOperationNodes.map(node => createExecutableResolverOperationNode(node, currentId++)); const newResolverDependencyMap = new Map(); for (const [key, nodes] of resolverDependencyFieldMap) { newResolverDependencyMap.set(key, nodes.map(node => createExecutableResolverOperationNode(node, currentId++))); } return { resolverOperationNodes: newResolverOperationNodes, resolverDependencyFieldMap: newResolverDependencyMap, }; } export function createResolverOperationNodeFromExecutable(executableNode, currentId) { const resolverOpNode = { subgraph: executableNode.subgraph, resolverOperationDocument: executableNode.resolverOperationDocument, resolverDependencies: [], resolverDependencyFieldMap: executableNode.resolverDependencyFieldMap, resolverPreDependencies: [], }; resolverOpNode.resolverDependencies = executableNode.resolverDependencies.map(node => createResolverOperationNodeFromExecutable(node, currentId++)); resolverOpNode.resolverDependencyFieldMap = new Map([...executableNode.resolverDependencyFieldMap.entries()].map(([key, value]) => [ key, value.map(createResolverOperationNodeFromExecutable), ])); if (executableNode.batchedResolverDependencies.length) { resolverOpNode.batch = true; for (const batchedResolverDependency of executableNode.batchedResolverDependencies) { resolverOpNode.resolverDependencies.push(createResolverOperationNodeFromExecutable(batchedResolverDependency, currentId++)); } } resolverOpNode.resolverPreDependencies = executableNode.resolverPreDependencies.map(node => createResolverOperationNodeFromExecutable(node, currentId++)); if (executableNode.batchedPreResolverDependencies.length) { resolverOpNode.batch = true; for (const batchedPreResolverDependency of executableNode.batchedPreResolverDependencies) { resolverOpNode.resolverPreDependencies.push(createResolverOperationNodeFromExecutable(batchedPreResolverDependency, currentId++)); } } if (executableNode.batchedResolverDependencyFieldMap.size) { resolverOpNode.batch = true; for (const [key, value] of executableNode.batchedResolverDependencyFieldMap) { resolverOpNode.resolverDependencyFieldMap.set(key, value.map(createResolverOperationNodeFromExecutable)); } } return resolverOpNode; } export function executeResolverOperationNodesWithDependenciesInParallel({ resolverOperationNodes, fieldDependencyMap, inputVariableMap, onExecute, obj = {}, context, path, errors, }) { const dependencyPromises = []; const asyncIterables = []; const outputVariableMap = new Map(); for (const depOp of resolverOperationNodes) { const depOpResult$ = executeResolverOperationNode({ resolverOperationNode: depOp, inputVariableMap, onExecute, context, path, errors, }); function handleDepOpResult(depOpResult) { if (depOpResult?.exported != null) { if (Array.isArray(depOpResult.exported)) { if (Array.isArray(obj)) { for (const index in depOpResult.exported) { Object.assign(obj[index], depOpResult.exported[index]); } } else { Object.assign(obj, ...depOpResult.exported); } } else { Object.assign(obj, depOpResult.exported); } for (const [key, value] of depOpResult.outputVariableMap) { outputVariableMap.set(key, value); } } } if (isAsyncIterable(depOpResult$)) { asyncIterables.push(mapAsyncIterator(depOpResult$, handleDepOpResult)); } else if (isPromise(depOpResult$)) { dependencyPromises.push(depOpResult$.then(handleDepOpResult)); } else { handleDepOpResult(depOpResult$); } } for (const [fieldName, fieldOperationNodes] of fieldDependencyMap) { const fieldOpPromises = []; const fieldOpAsyncIterables = []; const fieldOpResults = []; let listed = false; for (const fieldOperationNode of fieldOperationNodes) { const fieldOpResult$ = executeResolverOperationNode({ resolverOperationNode: fieldOperationNode, inputVariableMap, onExecute, context, path: [...path, fieldName], errors, }); function handleFieldOpResult(fieldOpResult) { if (fieldOpResult != null) { if (fieldOpResult.listed) { listed = true; } fieldOpResults.push(fieldOpResult.exported); } } if (isAsyncIterable(fieldOpResult$)) { fieldOpAsyncIterables.push(mapAsyncIterator(fieldOpResult$, handleFieldOpResult)); } else if (isPromise(fieldOpResult$)) { fieldOpPromises.push(fieldOpResult$.then(handleFieldOpResult)); } else { handleFieldOpResult(fieldOpResult$); } } function handleFieldOpResults() { if (listed) { const existingVals = arrayGet(obj, fieldName.split('.'), Array.isArray(fieldOpResults[0]?.[0]) ? 'array' : 'object'); for (const resultItemIndex in existingVals) { const fieldOpItemResults = fieldOpResults.map(resultItem => resultItem[resultItemIndex]); let existingVal = existingVals[resultItemIndex]; if (!existingVal) { existingVal = Array.isArray(fieldOpItemResults[0]) ? [] : {}; existingVals[resultItemIndex] = existingVal; } if (Array.isArray(existingVal)) { for (const existingValItemIndex in existingVal) { const existingValItem = existingVal[existingValItemIndex]; if (Array.isArray(existingValItem)) { for (const existingValItemItemIndex in existingValItem) { const existingValItemItem = existingValItem[existingValItemItemIndex]; Object.assign(existingValItemItem, ...fieldOpItemResults .map(fieldOpItemResult => fieldOpItemResult[existingValItemIndex][existingValItemItemIndex]) .flat(Infinity)); } } else { Object.assign(existingValItem, ...fieldOpItemResults .map(fieldOpItemResult => fieldOpItemResult[existingValItemIndex]) .flat(Infinity)); } } } else { Object.assign(existingVal, ...fieldOpItemResults.flat(Infinity)); } } } else { const existingVal = _.get(obj, fieldName); if (existingVal != null) { Object.assign(existingVal, ...fieldOpResults.flat(Infinity)); } else if (fieldOpResults.length) { _.set(obj, fieldName, fieldOpResults.length > 1 ? Object.assign(...fieldOpResults.flat(Infinity)) : fieldOpResults[0]); } } } if (fieldOpAsyncIterables.length) { const mergedIterable = Repeater.merge([...fieldOpPromises, ...fieldOpAsyncIterables]); asyncIterables.push(mapAsyncIterator(mergedIterable, handleFieldOpResults)); } else if (fieldOpPromises.length) { dependencyPromises.push(Promise.all(fieldOpPromises).then(handleFieldOpResults)); } else { handleFieldOpResults(); } } function handleDependencyPromises() { return { exported: obj, outputVariableMap, }; } if (asyncIterables.length) { const mergedIterable = Repeater.merge([...dependencyPromises, ...asyncIterables]); return mapAsyncIterator(mergedIterable, handleDependencyPromises); } if (dependencyPromises.length) { return Promise.all(dependencyPromises).then(handleDependencyPromises); } return handleDependencyPromises(); } export function executeResolverOperationNode({ resolverOperationNode, inputVariableMap, onExecute, context, path, errors, }) { const variablesForOperation = {}; const inputVarMapWithPreDeps = new Map(inputVariableMap); function handlePreDepResults() { for (const requiredVarName of resolverOperationNode.requiredVariableNames) { const varValue = inputVarMapWithPreDeps.get(requiredVarName); if (Array.isArray(varValue) && !resolverOperationNode.batch) { const promises = []; const asyncIterables = []; const results = []; const outputVariableMaps = []; for (const varIndex in varValue) { const itemInputVariableMap = new Map(); for (const [key, value] of inputVarMapWithPreDeps) { itemInputVariableMap.set(key, Array.isArray(value) ? value[varIndex] : value); } const itemResult$ = executeResolverOperationNode({ resolverOperationNode, inputVariableMap: itemInputVariableMap, onExecute, context, path: [...path, varIndex], errors, }); function handleItemResult(itemResult) { if (itemResult != null) { results[varIndex] = itemResult.exported; outputVariableMaps[varIndex] = itemResult.outputVariableMap; } } if (isAsyncIterable(itemResult$)) { asyncIterables.push(mapAsyncIterator(itemResult$, handleItemResult)); } else if (isPromise(itemResult$)) { promises.push(itemResult$.then(handleItemResult)); } else { handleItemResult(itemResult$); } } function handleResults() { const outputVariableMap = new Map(); for (const outputVariableMapItem of outputVariableMaps) { for (const [key, value] of outputVariableMapItem) { let existing = outputVariableMap.get(key); if (!existing) { existing = []; outputVariableMap.set(key, existing); } existing.push(value); } } return { exported: results, listed: true, outputVariableMap, }; } if (asyncIterables.length) { const mergedIterable = Repeater.merge([...promises, ...asyncIterables]); return mapAsyncIterator(mergedIterable, handleResults); } if (promises.length) { return Promise.all(promises).then(handleResults); } return handleResults(); } if (varValue != null) { variablesForOperation[requiredVarName] = varValue; } } const result$ = onExecute(resolverOperationNode.subgraph, resolverOperationNode.resolverOperationDocument, variablesForOperation, context); function handleResult(result) { result?.errors?.forEach((error) => { error = deserializeGraphQLError(error); const errorPath = [ ...path, ...(error.path?.filter((p) => p.toString() !== '__export') || []), ]; error = relocatedError(error, errorPath); error.extensions ||= {}; error.extensions.planNodeId = resolverOperationNode.id; errors.push(error); }); if (result?.data == null) { return null; } const outputVariableMap = new Map(); const exported = _.get(result.data, resolverOperationNode.exportPath); function handleExportedListForBatching(exportedList) { for (const [providedVariableName, providedVariablePath,] of resolverOperationNode.providedVariablePathMap) { const value = arrayGet(exportedList, providedVariablePath); outputVariableMap.set(providedVariableName, value); } return executeResolverOperationNodesWithDependenciesInParallel({ resolverOperationNodes: resolverOperationNode.batchedResolverDependencies, fieldDependencyMap: resolverOperationNode.batchedResolverDependencyFieldMap, inputVariableMap: outputVariableMap, onExecute, obj: exportedList, context, path, errors, }); } function handleExportedItem(exportedItem) { for (const [providedVariableName, providedVariablePath,] of resolverOperationNode.providedVariablePathMap) { const value = arrayGet(exportedItem, providedVariablePath); outputVariableMap.set(providedVariableName, value); } return executeResolverOperationNodesWithDependenciesInParallel({ resolverOperationNodes: resolverOperationNode.resolverDependencies, fieldDependencyMap: resolverOperationNode.resolverDependencyFieldMap, inputVariableMap: outputVariableMap, onExecute, obj: exportedItem, context, path, errors, }); } let depsResult$; if (Array.isArray(exported)) { const depsAsyncIterables = []; const depsResultPromises = []; const exportedListForBatching$ = handleExportedListForBatching(exported); if (isAsyncIterable(exportedListForBatching$)) { depsAsyncIterables.push(exportedListForBatching$); } else if (isPromise(exportedListForBatching$)) { depsResultPromises.push(exportedListForBatching$); } for (const exportedItem of exported) { const depsResultItem$ = handleExportedItem(exportedItem); if (isAsyncIterable(depsResultItem$)) { depsAsyncIterables.push(depsResultItem$); } else if (isPromise(depsResultItem$)) { depsResultPromises.push(depsResultItem$); } } if (depsAsyncIterables.length) { depsResult$ = Repeater.merge([...depsResultPromises, ...depsAsyncIterables]); } else if (depsResultPromises.length) { depsResult$ = Promise.all(depsResultPromises); } } else { depsResult$ = handleExportedItem(exported); } if (isAsyncIterable(depsResult$)) { return mapAsyncIterator(depsResult$, () => ({ exported, outputVariableMap, })); } else if (isPromise(depsResult$)) { return depsResult$.then(() => ({ exported, outputVariableMap, })); } return { exported, outputVariableMap, }; } if (resolverOperationNode.defer) { return new Repeater(async (push, stop) => { await push({ exported: null, outputVariableMap: new Map(), }); try { const result = await result$; if (isAsyncIterable(result)) { for await (const item of result) { const handledResult = await handleResult(item); if (isAsyncIterable(handledResult)) { for await (const item of handledResult) { await push(item); } } else { await push(handledResult); } } } const handledResult = await handleResult(result); if (isAsyncIterable(handledResult)) { for await (const item of handledResult) { await push(item); } } else { await push(handledResult); } return stop(); } catch (e) { return stop(e); } }); } if (isAsyncIterable(result$)) { return mapAsyncIterator(result$, handleResult); } if (isPromise(result$)) { return result$.then(handleResult); } return handleResult(result$); } const preDepPromises = []; function handlePreDepResultItem(preDepResult) { preDepResult.outputVariableMap.forEach((value, key) => { inputVarMapWithPreDeps.set(key, value); }); } for (const preDependencyNode of resolverOperationNode.resolverPreDependencies) { const preDepResult$ = executeResolverOperationNode({ resolverOperationNode: preDependencyNode, inputVariableMap, onExecute, context, path, errors, }); if (isPromise(preDepResult$)) { preDepPromises.push(preDepResult$.then(handlePreDepResultItem)); } else if (isAsyncIterable(preDepResult$)) { throw new Error('AsyncIterable not supported for preDepResult'); } else { handlePreDepResultItem(preDepResult$); } } if (preDepPromises.length) { return Promise.all(preDepPromises).then(handlePreDepResults); } return handlePreDepResults(); } // TODO: Maybe can be implemented in a better way function arrayGet(obj, path, setIfEmpty = false) { if (Array.isArray(obj)) { return obj.map(item => arrayGet(item, path, setIfEmpty)); } if (path.length === 1) { const existingVal = _.get(obj, path); if (existingVal == null && setIfEmpty) { const newVal = setIfEmpty === 'array' ? [] : {}; _.set(obj, path, newVal); return newVal; } return existingVal; } return arrayGet(_.get(obj, path[0]), path.slice(1), setIfEmpty); }