UNPKG

@graphql-tools/delegate

Version:

A set of utils for faster development of GraphQL tools

226 lines (225 loc) • 10.4 kB
import { defaultFieldResolver, Kind, responsePathAsArray, } from 'graphql'; import { getResponseKeyFromInfo, isPromise } from '@graphql-tools/utils'; import { createDeferred, getPlanLeftOverFromParent } from './leftOver.js'; import { getSubschema, getUnpathedErrors, handleResolverResult, isExternalObject, } from './mergeFields.js'; import { resolveExternalValue } from './resolveExternalValue.js'; import { FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols.js'; /** * Resolver that knows how to: * a) handle aliases for proxied schemas * b) handle errors from proxied schemas * c) handle external to internal enum conversion */ export function defaultMergedResolver(parent, args, context, info) { if (!parent) { return null; } const responseKey = getResponseKeyFromInfo(info); // check to see if parent is not a proxied result, i.e. if parent resolver was manually overwritten // See https://github.com/ardatan/graphql-tools/issues/967 if (!isExternalObject(parent)) { return defaultFieldResolver(parent, args, context, info); } // If the parent is satisfied for the left over after a nested delegation, try to resolve it if (!Object.prototype.hasOwnProperty.call(parent, responseKey)) { const leftOver = getPlanLeftOverFromParent(parent); // Add this field to the deferred fields if (leftOver) { let missingFieldNodes = leftOver.missingFieldsParentMap.get(parent); if (!missingFieldNodes) { missingFieldNodes = []; leftOver.missingFieldsParentMap.set(parent, missingFieldNodes); } missingFieldNodes.push(...info.fieldNodes.filter(fieldNode => leftOver.unproxiableFieldNodes.some(unproxiableFieldNode => unproxiableFieldNode === fieldNode))); let missingDeferredFields = leftOver.missingFieldsParentDeferredMap.get(parent); if (!missingDeferredFields) { missingDeferredFields = new Map(); leftOver.missingFieldsParentDeferredMap.set(parent, missingDeferredFields); } const deferred = createDeferred(); missingDeferredFields.set(responseKey, deferred); return deferred.promise; } return undefined; } return handleResult(parent, responseKey, context, info); } function handleResult(parent, responseKey, context, info) { const subschema = getSubschema(parent, responseKey); const data = parent[responseKey]; const unpathedErrors = getUnpathedErrors(parent); const resolvedData$ = resolveExternalValue(data, unpathedErrors, subschema, context, info); const leftOver = getPlanLeftOverFromParent(parent); // Handle possible deferred fields if any left over from the previous delegation plan is found if (leftOver) { if (isPromise(resolvedData$)) { return resolvedData$.then(resolvedData => { parent[responseKey] = resolvedData; handleLeftOver(parent, context, info, leftOver); return resolvedData; }); } parent[responseKey] = resolvedData$; handleLeftOver(parent, context, info, leftOver); } return resolvedData$; } function handleLeftOver(parent, context, info, leftOver) { const stitchingInfo = info.schema.extensions?.['stitchingInfo']; if (stitchingInfo) { for (const possibleSubschema of leftOver.nonProxiableSubschemas) { const parentTypeName = info.parentType.name; const selectionSet = stitchingInfo.mergedTypes[parentTypeName].selectionSets.get(possibleSubschema); // Wait until the parent is flattened, then check if non proxiable subschemas are satisfied now, // then the deferred fields can be resolved if (selectionSet) { const flattenedParent$ = flattenPromise(parent); if (isPromise(flattenedParent$)) { flattenedParent$.then(flattenedParent => { handleFlattenedParent(flattenedParent, possibleSubschema, selectionSet, leftOver, stitchingInfo, parentTypeName, context, info); }); } else { handleFlattenedParent(flattenedParent$, possibleSubschema, selectionSet, leftOver, stitchingInfo, parentTypeName, context, info); } } } } } function handleFlattenedParent(flattenedParent, possibleSubschema, selectionSet, leftOver, stitchingInfo, parentTypeName, context, info) { // If this subschema is satisfied now, try to resolve the deferred fields if (parentSatisfiedSelectionSet(flattenedParent, selectionSet)) { for (const [leftOverParent, missingFieldNodes] of leftOver.missingFieldsParentMap) { const resolver = stitchingInfo.mergedTypes[parentTypeName].resolvers.get(possibleSubschema); if (resolver) { try { // Extend the left over parent with missing fields Object.assign(leftOverParent, flattenedParent); const selectionSet = { kind: Kind.SELECTION_SET, selections: missingFieldNodes, }; const resolverResult$ = resolver(leftOverParent, context, info, possibleSubschema, selectionSet, info.parentType, info.parentType); // Resolve the deferred fields if they are resolved if (isPromise(resolverResult$)) { resolverResult$.then(resolverResult => handleDeferredResolverResult(resolverResult, possibleSubschema, selectionSet, leftOverParent, leftOver, context, info)).catch(error => handleDeferredResolverFailure(leftOver, leftOverParent, error)); } else { handleDeferredResolverResult(resolverResult$, possibleSubschema, selectionSet, leftOverParent, leftOver, context, info); } } catch (error) { handleDeferredResolverFailure(leftOver, leftOverParent, error); } } } } } function handleDeferredResolverResult(resolverResult, possibleSubschema, selectionSet, leftOverParent, leftOver, context, info) { handleResolverResult(resolverResult, possibleSubschema, selectionSet, leftOverParent, leftOverParent[FIELD_SUBSCHEMA_MAP_SYMBOL], info, responsePathAsArray(info.path), leftOverParent[UNPATHED_ERRORS_SYMBOL]); const deferredFields = leftOver.missingFieldsParentDeferredMap.get(leftOverParent); if (deferredFields) { for (const [responseKey, deferred] of deferredFields) { // If the deferred field is resolved, resolve the deferred field if (Object.prototype.hasOwnProperty.call(resolverResult, responseKey)) { deferred.resolve(handleResult(leftOverParent, responseKey, context, info)); } } leftOver.missingFieldsParentDeferredMap.delete(leftOverParent); } } function handleDeferredResolverFailure(leftOver, leftOverParent, error) { const deferredFields = leftOver.missingFieldsParentDeferredMap.get(leftOverParent); if (deferredFields) { for (const [_responseKey, deferred] of deferredFields) { deferred.reject(error); } leftOver.missingFieldsParentDeferredMap.delete(leftOverParent); } } function parentSatisfiedSelectionSet(parent, selectionSet) { if (Array.isArray(parent)) { const subschemas = new Set(); for (const item of parent) { const satisfied = parentSatisfiedSelectionSet(item, selectionSet); if (satisfied === undefined) { return undefined; } for (const subschema of satisfied) { subschemas.add(subschema); } } return subschemas; } if (parent === null) { return new Set(); } if (parent === undefined) { return undefined; } const subschemas = new Set(); for (const selection of selectionSet.selections) { if (selection.kind === Kind.FIELD) { const responseKey = selection.alias?.value ?? selection.name.value; if (parent[responseKey] === undefined) { return undefined; } if (isExternalObject(parent)) { const subschema = getSubschema(parent, responseKey); if (subschema) { subschemas.add(subschema); } } if (parent[responseKey] === null) { continue; } if (selection.selectionSet != null) { const satisfied = parentSatisfiedSelectionSet(parent[responseKey], selection.selectionSet); if (satisfied === undefined) { return undefined; } for (const subschema of satisfied) { subschemas.add(subschema); } } } else if (selection.kind === Kind.INLINE_FRAGMENT) { const inlineSatisfied = parentSatisfiedSelectionSet(parent, selection.selectionSet); if (inlineSatisfied === undefined) { return undefined; } for (const subschema of inlineSatisfied) { subschemas.add(subschema); } } } return subschemas; } function flattenPromise(data) { if (isPromise(data)) { return data.then(flattenPromise); } if (Array.isArray(data)) { return Promise.all(data.map(flattenPromise)); } if (data != null && typeof data === 'object') { const jobs = []; const newData = {}; for (const key in data) { const keyResult = flattenPromise(data[key]); if (isPromise(keyResult)) { jobs.push(keyResult.then(resolvedKeyResult => { newData[key] = resolvedKeyResult; })); } else { newData[key] = keyResult; } } if (jobs.length) { return Promise.all(jobs).then(() => newData); } return newData; } return data; }