@graphql-mesh/utils
Version:
323 lines (322 loc) • 17.2 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveAdditionalResolversWithoutImport = resolveAdditionalResolversWithoutImport;
exports.resolveAdditionalResolvers = resolveAdditionalResolvers;
const tslib_1 = require("tslib");
const dset_1 = require("dset");
const graphql_1 = require("graphql");
const lodash_get_1 = tslib_1.__importDefault(require("lodash.get"));
const lodash_topath_1 = tslib_1.__importDefault(require("lodash.topath"));
const cross_helpers_1 = require("@graphql-mesh/cross-helpers");
const string_interpolation_1 = require("@graphql-mesh/string-interpolation");
const types_1 = require("@graphql-mesh/types");
const delegate_1 = require("@graphql-tools/delegate");
const utils_1 = require("@graphql-tools/utils");
const promise_helpers_1 = require("@whatwg-node/promise-helpers");
const load_from_module_export_expression_js_1 = require("./load-from-module-export-expression.js");
const selectionSet_js_1 = require("./selectionSet.js");
const with_filter_js_1 = require("./with-filter.js");
function getTypeByPath(type, path) {
if ('ofType' in type) {
return getTypeByPath((0, graphql_1.getNamedType)(type), path);
}
if (path.length === 0) {
return (0, graphql_1.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 () => (0, utils_1.parseSelectionSet)(additionalResolver.sourceSelectionSet);
// If result path provided without a selectionSet
}
else if (additionalResolver.result) {
const resultPath = (0, lodash_topath_1.default)(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 ((0, graphql_1.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 = (0, graphql_1.getNamedType)(targetField.type);
abstractResultTypeName = targetFieldType?.name;
}
if (abstractResultTypeName !== resultFieldType.name) {
const abstractResultType = schema.getType(abstractResultTypeName);
if (((0, graphql_1.isInterfaceType)(abstractResultType) || (0, graphql_1.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: graphql_1.Kind.SELECTION_SET,
selections: [
{
kind: graphql_1.Kind.INLINE_FRAGMENT,
typeCondition: {
kind: graphql_1.Kind.NAMED_TYPE,
name: {
kind: graphql_1.Kind.NAME,
value: abstractResultTypeName,
},
},
selectionSet: finalSelectionSet,
},
],
};
}
finalSelectionSet = {
kind: graphql_1.Kind.SELECTION_SET,
selections: [
{
// we create a wrapping AST Field
kind: graphql_1.Kind.FIELD,
name: {
kind: graphql_1.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 (0, lodash_get_1.default)(result, resultExpression);
};
}
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: cross_helpers_1.process.env };
const topic = string_interpolation_1.stringInterpolator.parse(pubsubTopic, resolverData);
const ps = context?.pubsub || pubsub;
if ((0, types_1.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 = (0, with_filter_js_1.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 = (0, graphql_1.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 = (0, selectionSet_js_1.selectionSetOfData)(resolvePayload(payload));
const missingSelectionSet = (0, delegate_1.subtractSelectionSets)(requestedSelSet, availableSelSet);
if (!missingSelectionSet.selections.length) {
// all of the fields are already in the payload
return resolvePayload(payload);
}
// find the best resolver by diffing the selection sets
let resolver = null;
let subschema = null;
for (const [requiredSubschema, requiredSelSet] of mergedTypeInfo.selectionSets) {
const matchResolver = mergedTypeInfo?.resolvers.get(requiredSubschema);
if (!matchResolver) {
// the subschema has no resolvers, nothing to search for
continue;
}
const diff = (0, delegate_1.subtractSelectionSets)(requiredSelSet, availableSelSet);
if (!diff.selections.length) {
// all of the fields of the requesting (available) selection set is exist in the required selection set
resolver = matchResolver;
subschema = requiredSubschema;
break;
}
}
if (!resolver || !subschema) {
// the type cannot be resolved
return resolvePayload(payload);
}
return (0, promise_helpers_1.handleMaybePromise)(() => resolver(payload, ctx, info, subschema, missingSelectionSet, undefined, info.returnType), resolved => resolvePayload((0, utils_1.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: cross_helpers_1.process.env };
const targetArgs = {};
for (const argPath in additionalResolver.additionalArgs || {}) {
const value = additionalResolver.additionalArgs[argPath];
(0, dset_1.dset)(targetArgs, argPath, typeof value === 'string' ? string_interpolation_1.stringInterpolator.parse(value, resolverData) : value);
}
const options = {
...baseOptions,
root,
context,
info,
argsFromKeys: (keys) => {
const args = {};
(0, dset_1.dset)(args, additionalResolver.keysArg, keys);
Object.assign(args, targetArgs);
return args;
},
key: (0, lodash_get_1.default)(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: cross_helpers_1.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;
}
}
function resolveAdditionalResolvers(baseDir, additionalResolvers, importFn, pubsub) {
return Promise.all((additionalResolvers || []).map(async (additionalResolver) => {
if (typeof additionalResolver === 'string') {
const resolvers = await (0, load_from_module_export_expression_js_1.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') {
(0, dset_1.dset)(args, path, string_interpolation_1.stringInterpolator.parse(value.toString(), resolverData));
}
else {
for (const key in value) {
deeplySetArgs(resolverData, args, `${path}.${key}`, value[key]);
}
}
}
;