@graphql-mesh/fusion-execution
Version:
Runtime for Fusion Supergraph
549 lines (548 loc) • 24.2 kB
JavaScript
/* 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);
}