@graphql-mesh/utils
Version:
397 lines (396 loc) • 18.6 kB
JavaScript
import { dset } from 'dset';
import { getNamedType, getNullableType, isAbstractType, isInterfaceType, isListType, 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 { parseSelectionSet } from '@graphql-tools/utils';
import { handleMaybePromise, mapAsyncIterator } from '@whatwg-node/promise-helpers';
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 ensurePathInSelectionSet(selectionSet, path) {
if (path.length === 0) {
return (selectionSet ?? {
kind: 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 === Kind.FIELD && !selection.alias && selection.name.value === head);
if (existingField && existingField.kind === Kind.FIELD) {
if (rest.length === 0) {
return {
kind: Kind.SELECTION_SET,
selections,
};
}
const nestedSelectionSet = ensurePathInSelectionSet(existingField.selectionSet, rest);
return {
kind: Kind.SELECTION_SET,
selections: selections.map(selection => selection === existingField
? {
...existingField,
selectionSet: nestedSelectionSet,
}
: selection),
};
}
const newField = rest.length === 0
? {
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: head,
},
}
: {
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: head,
},
selectionSet: ensurePathInSelectionSet(undefined, rest),
};
return {
kind: Kind.SELECTION_SET,
selections: [...selections, newField],
};
}
function generateSelectionSetFactory(schema, additionalResolver) {
const valueKeyField = 'valueKeyField' in additionalResolver ? additionalResolver.valueKeyField : undefined;
const valueKeyPath = valueKeyField ? toPath(valueKeyField) : undefined;
const ensureValueKeyField = (subtree) => {
if (!valueKeyPath?.length) {
return (subtree ?? {
kind: Kind.SELECTION_SET,
selections: [],
});
}
return ensurePathInSelectionSet(subtree, valueKeyPath);
};
if (additionalResolver.sourceSelectionSet) {
return () => ensureValueKeyField(parseSelectionSet(additionalResolver.sourceSelectionSet));
// If result path provided without a selectionSet
}
else if (additionalResolver.result) {
const resultPath = toPath(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 && (isObjectType(sourceType) || isInterfaceType(sourceType))
? sourceType.getFields()[additionalResolver.sourceFieldName]
: undefined;
const resultFieldType = sourceField?.type
? getTypeByPath(sourceField.type, resultPath)
: undefined;
if (resultFieldType && 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 = 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: 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;
};
}
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 lodashGet(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 = lodashGet(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;
});
};
}
export function getResolverForPubSubOperation(opts, valuesFromResults) {
const pubsubTopic = opts.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 || opts?.pubsub;
if (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 = withFilter(subscribeFn, filterFunction);
}
return {
subscribe: subscribeFn,
resolve: (payload) => (valuesFromResults ? valuesFromResults(payload) : payload),
};
}
export 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 handleMaybePromise(() => subscribe(...args), iterator => 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, isListType(getNullableType(info.returnType)));
baseOptions.valuesFromResultsByKeyReturnType = info.returnType;
}
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 = {
selectionSet: baseOptions.selectionSet,
valuesFromResults: baseOptions.valuesFromResults,
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]);
}
}
}