UNPKG

@graphql-mesh/utils

Version:
403 lines (402 loc) • 19.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getResolverForPubSubOperation = getResolverForPubSubOperation; 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 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 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 ensurePathInSelectionSet(selectionSet, path) { if (path.length === 0) { return (selectionSet ?? { kind: graphql_1.Kind.SELECTION_SET, selections: [], }); } const [head, ...rest] = path; const selections = selectionSet?.selections ? [...selectionSet.selections] : []; // Reuse an existing field even if it has @skip/@include (or other directives). // Appending a second bare field with the same name would violate GraphQL field merging. const existingField = selections.find(selection => selection.kind === graphql_1.Kind.FIELD && !selection.alias && selection.name.value === head); if (existingField && existingField.kind === graphql_1.Kind.FIELD) { if (rest.length === 0) { return { kind: graphql_1.Kind.SELECTION_SET, selections, }; } const nestedSelectionSet = ensurePathInSelectionSet(existingField.selectionSet, rest); return { kind: graphql_1.Kind.SELECTION_SET, selections: selections.map(selection => selection === existingField ? { ...existingField, selectionSet: nestedSelectionSet, } : selection), }; } const newField = rest.length === 0 ? { kind: graphql_1.Kind.FIELD, name: { kind: graphql_1.Kind.NAME, value: head, }, } : { kind: graphql_1.Kind.FIELD, name: { kind: graphql_1.Kind.NAME, value: head, }, selectionSet: ensurePathInSelectionSet(undefined, rest), }; return { kind: graphql_1.Kind.SELECTION_SET, selections: [...selections, newField], }; } function generateSelectionSetFactory(schema, additionalResolver) { const valueKeyField = 'valueKeyField' in additionalResolver ? additionalResolver.valueKeyField : undefined; const valueKeyPath = valueKeyField ? (0, lodash_topath_1.default)(valueKeyField) : undefined; const ensureValueKeyField = (subtree) => { if (!valueKeyPath?.length) { return (subtree ?? { kind: graphql_1.Kind.SELECTION_SET, selections: [], }); } return ensurePathInSelectionSet(subtree, valueKeyPath); }; if (additionalResolver.sourceSelectionSet) { return () => ensureValueKeyField((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; // `info.schema` is the gateway/supergraph schema. Encapsulate (and similar // transforms) keep the real source field as an @inaccessible copy such as // `_encapsulated_<name>_<field>`, which federation then strips from the // supergraph. Selection-set wrapping from `result` still works without that // field; we only need its type when the projected path is abstract. const sourceType = schema.getType(additionalResolver.sourceTypeName); const sourceField = sourceType && ((0, graphql_1.isObjectType)(sourceType) || (0, graphql_1.isInterfaceType)(sourceType)) ? sourceType.getFields()[additionalResolver.sourceFieldName] : undefined; const resultFieldType = sourceField?.type ? getTypeByPath(sourceField.type, resultPath) : undefined; if (resultFieldType && (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 = ensureValueKeyField(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 && resultFieldType && 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; }; } else if (valueKeyPath?.length) { return (subtree) => ensureValueKeyField(subtree); } 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 generateValuesFromResultsByKey(valueKeyField, resultExpression, isListReturnType) { const extractResult = resultExpression ? generateValuesFromResults(resultExpression) : undefined; return function valuesFromResults(results, keys = []) { const extracted = extractResult ? extractResult(results) : results; const items = Array.isArray(extracted) ? extracted : extracted == null ? [] : [extracted]; const matchesByKey = new Map(); for (const item of items) { if (item == null) { continue; } const resultKey = (0, lodash_get_1.default)(item, valueKeyField); if (resultKey == null) { continue; } const bucket = matchesByKey.get(resultKey); if (bucket) { bucket.push(item); } else { matchesByKey.set(resultKey, [item]); } } return keys.map(key => { const matches = matchesByKey.get(key); if (isListReturnType) { return matches ?? []; } return matches?.[0] ?? null; }); }; } function getResolverForPubSubOperation(opts, valuesFromResults) { const pubsubTopic = opts.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 || opts?.pubsub; if ((0, types_1.isHivePubSub)(ps)) { return ps.subscribe(topic)[Symbol.asyncIterator](); } return ps.asyncIterator(topic)[Symbol.asyncIterator](); }; if (opts.filterBy) { let filterFunction; try { // eslint-disable-next-line no-new-func filterFunction = new Function('root', 'args', 'context', 'info', `return ${opts.filterBy};`); } catch (e) { throw new Error(`Error while parsing filterBy expression "${opts.filterBy}" in additional subscription resolver: ${e.message}`); } subscribeFn = (0, with_filter_js_1.withFilter)(subscribeFn, filterFunction); } return { subscribe: subscribeFn, resolve: (payload) => (valuesFromResults ? valuesFromResults(payload) : payload), }; } function resolveAdditionalResolversWithoutImport(additionalResolver, pubsub) { const baseOptions = { valuesFromResults: additionalResolver.result ? generateValuesFromResults(additionalResolver.result) : undefined, }; if ('pubsubTopic' in additionalResolver) { const { subscribe, resolve } = getResolverForPubSubOperation({ pubsubTopic: additionalResolver.pubsubTopic, pubsub, filterBy: additionalResolver.filterBy, result: additionalResolver.result, }, baseOptions.valuesFromResults); return { [additionalResolver.targetTypeName]: { [additionalResolver.targetFieldName]: { subscribe: (...args) => { // pubsub emits the raw value, but graphql-js passes each subscription event through the root // field resolver and expects the event to be shaped like { [fieldName]: value } return (0, promise_helpers_1.handleMaybePromise)(() => subscribe(...args), iterator => (0, promise_helpers_1.mapAsyncIterator)(iterator, payload => ({ [additionalResolver.targetFieldName]: payload, }))); }, resolve: (payload, ...args) => resolve(payload[additionalResolver.targetFieldName], // @ts-expect-error in case the resolver args expand, we're ready to pass ...args), }, }, }; } 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); } if (additionalResolver.valueKeyField && baseOptions.valuesFromResultsByKeyReturnType !== info.returnType) { baseOptions.valuesFromResults = generateValuesFromResultsByKey(additionalResolver.valueKeyField, additionalResolver.result, (0, graphql_1.isListType)((0, graphql_1.getNullableType)(info.returnType))); baseOptions.valuesFromResultsByKeyReturnType = info.returnType; } 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 = { selectionSet: baseOptions.selectionSet, valuesFromResults: baseOptions.valuesFromResults, 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]); } } }