UNPKG

@graphql-tools/delegate

Version:

A set of utils for faster development of GraphQL tools

150 lines (149 loc) • 6.98 kB
import { GraphQLError, isAbstractType, locatedError, responsePathAsArray, } from 'graphql'; import { collectFields, isPromise, memoize1, mergeDeep, relocatedError, } from '@graphql-tools/utils'; import { leftOverByDelegationPlan, PLAN_LEFT_OVER } from './leftOver.js'; import { FIELD_SUBSCHEMA_MAP_SYMBOL, OBJECT_SUBSCHEMA_SYMBOL, UNPATHED_ERRORS_SYMBOL, } from './symbols.js'; export function isExternalObject(data) { return data[UNPATHED_ERRORS_SYMBOL] !== undefined; } export function annotateExternalObject(object, errors, subschema, subschemaMap) { Object.defineProperties(object, { [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, [FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: subschemaMap }, [UNPATHED_ERRORS_SYMBOL]: { value: errors }, }); return object; } export function getSubschema(object, responseKey) { return object[FIELD_SUBSCHEMA_MAP_SYMBOL]?.[responseKey] ?? object[OBJECT_SUBSCHEMA_SYMBOL]; } export function getUnpathedErrors(object) { return object[UNPATHED_ERRORS_SYMBOL]; } const EMPTY_ARRAY = []; const EMPTY_OBJECT = Object.create(null); export const getActualFieldNodes = memoize1(function (fieldNode) { return [fieldNode]; }); export function mergeFields(mergedTypeInfo, object, sourceSubschema, context, info) { const delegationMaps = mergedTypeInfo.delegationPlanBuilder(info.schema, sourceSubschema, info.variableValues != null && Object.keys(info.variableValues).length > 0 ? info.variableValues : EMPTY_OBJECT, info.fragments != null && Object.keys(info.fragments).length > 0 ? info.fragments : EMPTY_OBJECT, info.fieldNodes?.length ? info.fieldNodes.length === 1 ? getActualFieldNodes(info.fieldNodes[0]) : info.fieldNodes : EMPTY_ARRAY); const leftOver = leftOverByDelegationPlan.get(delegationMaps); if (leftOver) { object[PLAN_LEFT_OVER] = leftOver; } const res$ = delegationMaps.reduce((prev, delegationMap) => { function executeFn() { return executeDelegationStage(mergedTypeInfo, delegationMap, object, context, info); } if (isPromise(prev)) { return prev.then(executeFn); } return executeFn(); }, undefined); function handleDelegationPlanResult() { return object; } if (isPromise(res$)) { return res$.then(handleDelegationPlanResult); } return handleDelegationPlanResult(); } export function handleResolverResult(resolverResult, subschema, selectionSet, object, combinedFieldSubschemaMap, info, path, combinedErrors) { if (resolverResult instanceof Error || resolverResult == null) { const schema = subschema.transformedSchema || info.schema; const type = schema.getType(object.__typename); const { fields } = collectFields(schema, EMPTY_OBJECT, EMPTY_OBJECT, type, selectionSet); const nullResult = {}; for (const [responseKey, fieldNodes] of fields) { const combinedPath = [...path, responseKey]; if (resolverResult instanceof GraphQLError) { nullResult[responseKey] = relocatedError(resolverResult, combinedPath); } else if (resolverResult instanceof Error) { nullResult[responseKey] = locatedError(resolverResult, fieldNodes, combinedPath); } else { nullResult[responseKey] = null; } } resolverResult = nullResult; } else { if (resolverResult[UNPATHED_ERRORS_SYMBOL]) { combinedErrors.push(...resolverResult[UNPATHED_ERRORS_SYMBOL]); } } const objectSubschema = resolverResult[OBJECT_SUBSCHEMA_SYMBOL]; const fieldSubschemaMap = resolverResult[FIELD_SUBSCHEMA_MAP_SYMBOL]; for (const responseKey in resolverResult) { if (responseKey === '__proto__') { continue; } const existingPropValue = object[responseKey]; const sourcePropValue = resolverResult[responseKey]; if (responseKey === '__typename' && existingPropValue !== sourcePropValue && isAbstractType(subschema.transformedSchema.getType(sourcePropValue))) { continue; } if (sourcePropValue != null || existingPropValue == null) { if (existingPropValue != null && typeof existingPropValue === 'object' && Object.keys(existingPropValue).length > 0) { if (Array.isArray(existingPropValue) && Array.isArray(sourcePropValue) && existingPropValue.length === sourcePropValue.length) { object[responseKey] = existingPropValue.map((existingElement, index) => sourcePropValue instanceof Error ? existingElement : mergeDeep([existingElement, sourcePropValue[index]])); } else if (!(sourcePropValue instanceof Error)) { object[responseKey] = mergeDeep([existingPropValue, sourcePropValue]); } } else { object[responseKey] = sourcePropValue; } } combinedFieldSubschemaMap[responseKey] = fieldSubschemaMap?.[responseKey] ?? objectSubschema ?? subschema; } } function executeDelegationStage(mergedTypeInfo, delegationMap, object, context, info) { const combinedErrors = object[UNPATHED_ERRORS_SYMBOL]; const path = responsePathAsArray(info.path); const combinedFieldSubschemaMap = object[FIELD_SUBSCHEMA_MAP_SYMBOL]; const jobs = []; for (const [subschema, selectionSet] of delegationMap) { const schema = subschema.transformedSchema || info.schema; const type = schema.getType(object.__typename); const resolver = mergedTypeInfo.resolvers.get(subschema); if (resolver) { try { const resolverResult$ = resolver(object, context, info, subschema, selectionSet, undefined, type); if (isPromise(resolverResult$)) { jobs.push(resolverResult$.then(resolverResult => handleResolverResult(resolverResult, subschema, selectionSet, object, combinedFieldSubschemaMap, info, path, combinedErrors)).catch(error => handleResolverResult(error, subschema, selectionSet, object, combinedFieldSubschemaMap, info, path, combinedErrors))); } else { handleResolverResult(resolverResult$, subschema, selectionSet, object, combinedFieldSubschemaMap, info, path, combinedErrors); } } catch (error) { handleResolverResult(error, subschema, selectionSet, object, combinedFieldSubschemaMap, info, path, combinedErrors); } } } if (jobs.length) { if (jobs.length === 1) { return jobs[0]; } return Promise.all(jobs); } }