UNPKG

@graphql-mesh/utils

Version:
323 lines (322 loc) 17.2 kB
"use strict"; 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]); } } }