@graphql-mesh/utils
Version:
354 lines (353 loc) • 19 kB
JavaScript
import { dset } from 'dset';
import { getNamedType, GraphQLList, 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 { isHivePubSub, } from '@graphql-mesh/types';
import { batchDelegateToSchema } from '@graphql-tools/batch-delegate';
import { delegateToSchema, subtractSelectionSets, } from '@graphql-tools/delegate';
import { mergeDeep, parseSelectionSet } from '@graphql-tools/utils';
import { handleMaybePromise } from '@whatwg-node/promise-helpers';
import { loadFromModuleExportExpression } from './load-from-module-export-expression.js';
import { selectionSetOfData } from './selectionSet.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) {
const pubsubTopic = additionalResolver.pubsubTopic;
let subscribeFn = function subscriber(root, args, context, info) {
const resolverData = { root, args, context, info, env: process.env };
const topic = stringInterpolator.parse(pubsubTopic, resolverData);
const ps = context?.pubsub || pubsub;
if (isHivePubSub(ps)) {
return ps.subscribe(topic)[Symbol.asyncIterator]();
}
return ps.asyncIterator(topic)[Symbol.asyncIterator]();
};
if (additionalResolver.filterBy) {
let filterFunction;
try {
// eslint-disable-next-line no-new-func
filterFunction = new Function('root', 'args', 'context', 'info', `return ${additionalResolver.filterBy};`);
}
catch (e) {
throw new Error(`Error while parsing filterBy expression "${additionalResolver.filterBy}" in additional subscription resolver: ${e.message}`);
}
subscribeFn = withFilter(subscribeFn, filterFunction);
}
return {
[additionalResolver.targetTypeName]: {
[additionalResolver.targetFieldName]: {
subscribe: subscribeFn,
resolve: (payload, _, ctx, info) => {
function resolvePayload(payload) {
if (baseOptions.valuesFromResults) {
return baseOptions.valuesFromResults(payload);
}
return payload;
}
const stitchingInfo = info?.schema.extensions?.stitchingInfo;
if (!stitchingInfo) {
return resolvePayload(payload); // no stitching, cannot be resolved anywhere else
}
const returnTypeName = getNamedType(info.returnType).name;
const mergedTypeInfo = stitchingInfo.mergedTypes[returnTypeName];
if (!mergedTypeInfo) {
return resolvePayload(payload); // this type is not merged or resolvable
}
// we dont compare fragment definitions because they mean there are type-conditions
// more advanced behavior. if we encounter such a case, the missing selection set
// will have fields and we will perform a call to the subschema
const requestedSelSet = info.fieldNodes[0]?.selectionSet;
if (!requestedSelSet) {
return resolvePayload(payload); // should never happen, but hey 🤷♂️
}
const availableSelSet = selectionSetOfData(resolvePayload(payload));
const missingSelectionSet = subtractSelectionSets(requestedSelSet, availableSelSet);
if (!missingSelectionSet.selections.length) {
// all of the fields are already in the payload
return resolvePayload(payload);
}
// find the best subgraph by diffing the selection sets
let subschema = null;
let mergedTypeConfig = null;
for (const [requiredSubschema, requiredSelSet] of mergedTypeInfo.selectionSets) {
const tentativeMergedTypeConfig = requiredSubschema.merge?.[returnTypeName];
if (tentativeMergedTypeConfig?.fields) {
// this resolver requires additional fields (think `@requires(fields: "x")`)
// TODO: actually implement whether the payload already contains those fields
// TODO: is there a better way for finding a match?
continue;
}
const diff = subtractSelectionSets(requiredSelSet, availableSelSet);
if (!diff.selections.length) {
// all of the fields of the requesting (available) selection set is exist in the required selection set
subschema = requiredSubschema;
mergedTypeConfig = tentativeMergedTypeConfig;
break;
}
}
if (!subschema || !mergedTypeConfig) {
// the type cannot be resolved
return resolvePayload(payload);
}
return handleMaybePromise(() => {
if (mergedTypeConfig.argsFromKeys) {
return batchDelegateToSchema({
schema: subschema,
operation: 'query',
fieldName: mergedTypeConfig.fieldName,
returnType: new GraphQLList(info.returnType),
key: mergedTypeConfig.key?.(payload) || payload, // TODO: should use valueFromResults on the args too?
argsFromKeys: mergedTypeConfig.argsFromKeys,
valuesFromResults: mergedTypeConfig.valuesFromResults,
selectionSet: missingSelectionSet,
context: ctx,
info,
dataLoaderOptions: mergedTypeConfig.dataLoaderOptions,
skipTypeMerging: false, // important to be false so that fields outside this subgraph can be resolved properly
});
}
if (mergedTypeConfig.args) {
return delegateToSchema({
schema: subschema,
operation: 'query',
fieldName: mergedTypeConfig.fieldName,
returnType: info.returnType,
args: mergedTypeConfig.args(payload), // TODO: should use valueFromResults on the args too?
selectionSet: missingSelectionSet,
context: ctx,
info,
skipTypeMerging: false, // important to be false so that fields outside this subgraph can be resolved properly
});
}
// no way to delegate to anything, return empty - i.e. resolve just payload
// should not happen though, there'll be something to use
return {};
}, resolved => resolvePayload(mergeDeep([payload, resolved])));
},
},
},
};
}
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 || {}) {
const value = additionalResolver.additionalArgs[argPath];
dset(targetArgs, argPath, typeof value === 'string' ? stringInterpolator.parse(value, resolverData) : value);
}
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 (additionalResolver.sourceFieldName === '__typename') {
return additionalResolver.sourceTypeName;
}
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]);
}
}
}