UNPKG

@graphql-mesh/utils

Version:
252 lines (251 loc) • 12.2 kB
import { dset } from 'dset'; import { getNamedType, isAbstractType, isInterfaceType, isObjectType, Kind, } from 'graphql'; import lodashGet from 'lodash.get'; import toPath from 'lodash.topath'; import { process } from '@graphql-mesh/cross-helpers'; import { stringInterpolator } from '@graphql-mesh/string-interpolation'; import { parseSelectionSet } from '@graphql-tools/utils'; import { loadFromModuleExportExpression } from './load-from-module-export-expression.js'; import { withFilter } from './with-filter.js'; function getTypeByPath(type, path) { if ('ofType' in type) { return getTypeByPath(getNamedType(type), path); } if (path.length === 0) { return getNamedType(type); } if (!('getFields' in type)) { throw new Error(`${type} cannot have a path ${path.join('.')}`); } const fieldMap = type.getFields(); const currentFieldName = path[0]; // Might be an index of an array if (!Number.isNaN(parseInt(currentFieldName))) { return getTypeByPath(type, path.slice(1)); } const field = fieldMap[currentFieldName]; if (!field?.type) { throw new Error(`${type}.${currentFieldName} is not a valid field.`); } return getTypeByPath(field.type, path.slice(1)); } function generateSelectionSetFactory(schema, additionalResolver) { if (additionalResolver.sourceSelectionSet) { return () => parseSelectionSet(additionalResolver.sourceSelectionSet); // If result path provided without a selectionSet } else if (additionalResolver.result) { const resultPath = toPath(additionalResolver.result); let abstractResultTypeName; const sourceType = schema.getType(additionalResolver.sourceTypeName); const sourceTypeFields = sourceType.getFields(); const sourceField = sourceTypeFields[additionalResolver.sourceFieldName]; const resultFieldType = getTypeByPath(sourceField.type, resultPath); if (isAbstractType(resultFieldType)) { if (additionalResolver.resultType) { abstractResultTypeName = additionalResolver.resultType; } else { const targetType = schema.getType(additionalResolver.targetTypeName); const targetTypeFields = targetType.getFields(); const targetField = targetTypeFields[additionalResolver.targetFieldName]; const targetFieldType = getNamedType(targetField.type); abstractResultTypeName = targetFieldType?.name; } if (abstractResultTypeName !== resultFieldType.name) { const abstractResultType = schema.getType(abstractResultTypeName); if ((isInterfaceType(abstractResultType) || isObjectType(abstractResultType)) && !schema.isSubType(resultFieldType, abstractResultType)) { throw new Error(`${additionalResolver.sourceTypeName}.${additionalResolver.sourceFieldName}.${resultPath.join('.')} doesn't implement ${abstractResultTypeName}.}`); } } } return (subtree) => { let finalSelectionSet = subtree; let isLastResult = true; const resultPathReversed = [...resultPath].reverse(); for (const pathElem of resultPathReversed) { // Ensure the path elem is not array index if (Number.isNaN(parseInt(pathElem))) { if (isLastResult && abstractResultTypeName && abstractResultTypeName !== resultFieldType.name) { finalSelectionSet = { kind: Kind.SELECTION_SET, selections: [ { kind: Kind.INLINE_FRAGMENT, typeCondition: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: abstractResultTypeName, }, }, selectionSet: finalSelectionSet, }, ], }; } finalSelectionSet = { kind: Kind.SELECTION_SET, selections: [ { // we create a wrapping AST Field kind: Kind.FIELD, name: { kind: Kind.NAME, value: pathElem, }, // Inside the field selection selectionSet: finalSelectionSet, }, ], }; isLastResult = false; } } return finalSelectionSet; }; } return undefined; } function generateValuesFromResults(resultExpression) { return function valuesFromResults(result) { if (Array.isArray(result)) { return result.map(valuesFromResults); } return lodashGet(result, resultExpression); }; } export function resolveAdditionalResolversWithoutImport(additionalResolver, pubsub) { const baseOptions = {}; if (additionalResolver.result) { baseOptions.valuesFromResults = generateValuesFromResults(additionalResolver.result); } if ('pubsubTopic' in additionalResolver) { return { [additionalResolver.targetTypeName]: { [additionalResolver.targetFieldName]: { subscribe: withFilter((root, args, context, info) => { const resolverData = { root, args, context, info, env: process.env }; const topic = stringInterpolator.parse(additionalResolver.pubsubTopic, resolverData); return pubsub.asyncIterator(topic); }, (root, args, context, info) => { return additionalResolver.filterBy ? // eslint-disable-next-line no-new-func new Function(`return ${additionalResolver.filterBy}`)() : true; }), resolve: (payload) => { if (baseOptions.valuesFromResults) { return baseOptions.valuesFromResults(payload); } return payload; }, }, }, }; } else if ('keysArg' in additionalResolver) { return { [additionalResolver.targetTypeName]: { [additionalResolver.targetFieldName]: { selectionSet: additionalResolver.requiredSelectionSet || `{ ${additionalResolver.keyField} }`, resolve: async (root, args, context, info) => { if (!baseOptions.selectionSet) { baseOptions.selectionSet = generateSelectionSetFactory(info.schema, additionalResolver); } const resolverData = { root, args, context, info, env: process.env }; const targetArgs = {}; for (const argPath in additionalResolver.additionalArgs || {}) { dset(targetArgs, argPath, stringInterpolator.parse(additionalResolver.additionalArgs[argPath], resolverData)); } const options = { ...baseOptions, root, context, info, argsFromKeys: (keys) => { const args = {}; dset(args, additionalResolver.keysArg, keys); Object.assign(args, targetArgs); return args; }, key: lodashGet(root, additionalResolver.keyField), }; return context[additionalResolver.sourceName][additionalResolver.sourceTypeName][additionalResolver.sourceFieldName](options); }, }, }, }; } else if ('targetTypeName' in additionalResolver) { return { [additionalResolver.targetTypeName]: { [additionalResolver.targetFieldName]: { selectionSet: additionalResolver.requiredSelectionSet, resolve: (root, args, context, info) => { // Assert source exists if (!context[additionalResolver.sourceName]) { throw new Error(`No source found named "${additionalResolver.sourceName}"`); } if (!context[additionalResolver.sourceName][additionalResolver.sourceTypeName]) { throw new Error(`No root type found named "${additionalResolver.sourceTypeName}" exists in the source ${additionalResolver.sourceName}\n` + `It should be one of the following; ${Object.keys(context[additionalResolver.sourceName]).join(',')})}}`); } if (!context[additionalResolver.sourceName][additionalResolver.sourceTypeName][additionalResolver.sourceFieldName]) { throw new Error(`No field named "${additionalResolver.sourceFieldName}" exists in the type ${additionalResolver.sourceTypeName} from the source ${additionalResolver.sourceName}`); } if (!baseOptions.selectionSet) { baseOptions.selectionSet = generateSelectionSetFactory(info.schema, additionalResolver); } const resolverData = { root, args, context, info, env: process.env }; const targetArgs = {}; deeplySetArgs(resolverData, { targetArgs }, 'targetArgs', additionalResolver.sourceArgs); const options = { ...baseOptions, root, args: targetArgs, context, info, }; return context[additionalResolver.sourceName][additionalResolver.sourceTypeName][additionalResolver.sourceFieldName](options); }, }, }, }; } else { return additionalResolver; } } export function resolveAdditionalResolvers(baseDir, additionalResolvers, importFn, pubsub) { return Promise.all((additionalResolvers || []).map(async (additionalResolver) => { if (typeof additionalResolver === 'string') { const resolvers = await loadFromModuleExportExpression(additionalResolver, { cwd: baseDir, defaultExportName: 'resolvers', importFn, }); if (!resolvers) { console.warn(`Unable to load resolvers from file: ${additionalResolver}`); return {}; } return resolvers; } else { return resolveAdditionalResolversWithoutImport(additionalResolver, pubsub); } })); } function deeplySetArgs(resolverData, args, path, value) { if (typeof value === 'string') { dset(args, path, stringInterpolator.parse(value.toString(), resolverData)); } else { for (const key in value) { deeplySetArgs(resolverData, args, `${path}.${key}`, value[key]); } } }