UNPKG

graphql

Version:

A Query Language and Runtime which can target any service.

419 lines 20.2 kB
import { inspect } from "../../jsutils/inspect.mjs"; import { GraphQLError } from "../../error/GraphQLError.mjs"; import { Kind } from "../../language/kinds.mjs"; import { print } from "../../language/printer.mjs"; import { getNamedType, isInterfaceType, isLeafType, isListType, isNonNullType, isObjectType, } from "../../type/definition.mjs"; import { sortValueNode } from "../../utilities/sortValueNode.mjs"; import { typeFromAST } from "../../utilities/typeFromAST.mjs"; function reasonMessage(reason) { if (Array.isArray(reason)) { return reason .map(([responseName, subReason]) => `subfields "${responseName}" conflict because ` + reasonMessage(subReason)) .join(' and '); } return reason; } export function OverlappingFieldsCanBeMergedRule(context) { const comparedFieldsAndFragmentPairs = new OrderedPairSet(); const comparedFragmentPairs = new PairSet(); const cachedFieldsAndFragmentSpreads = new Map(); return { SelectionSet(selectionSet) { const conflicts = findConflictsWithinSelectionSet(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, context.getParentType(), selectionSet); for (const [[responseName, reason], fields1, fields2] of conflicts) { const reasonMsg = reasonMessage(reason); context.reportError(new GraphQLError(`Fields "${responseName}" conflict because ${reasonMsg}. Use different aliases on the fields to fetch both if this was intentional.`, { nodes: fields1.concat(fields2) })); } }, }; } function findConflictsWithinSelectionSet(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, parentType, selectionSet) { const conflicts = []; const [fieldMap, fragmentSpreads] = getFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, parentType, selectionSet, undefined); collectConflictsWithin(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, fieldMap); if (fragmentSpreads.length !== 0) { for (let i = 0; i < fragmentSpreads.length; i++) { collectConflictsBetweenFieldsAndFragment(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, false, fieldMap, fragmentSpreads[i]); for (let j = i + 1; j < fragmentSpreads.length; j++) { collectConflictsBetweenFragments(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, false, fragmentSpreads[i], fragmentSpreads[j]); } } } return conflicts; } function collectConflictsBetweenFieldsAndFragment(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fieldMap, fragmentSpread) { if (comparedFieldsAndFragmentPairs.has(fieldMap, fragmentSpread.key, areMutuallyExclusive)) { return; } comparedFieldsAndFragmentPairs.add(fieldMap, fragmentSpread.key, areMutuallyExclusive); const fragment = context.getFragment(fragmentSpread.node.name.value); if (!fragment) { return; } const [fieldMap2, referencedFragmentSpreads] = getReferencedFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, fragment, fragmentSpread.varMap); if (fieldMap === fieldMap2) { return; } collectConflictsBetween(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fieldMap, undefined, fieldMap2, fragmentSpread.varMap); for (const referencedFragmentSpread of referencedFragmentSpreads) { collectConflictsBetweenFieldsAndFragment(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fieldMap, referencedFragmentSpread); } } function collectConflictsBetweenFragments(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fragmentSpread1, fragmentSpread2) { if (fragmentSpread1.key === fragmentSpread2.key) { return; } if (fragmentSpread1.node.name.value === fragmentSpread2.node.name.value) { if (!sameArguments(fragmentSpread1.node.arguments, fragmentSpread1.varMap, fragmentSpread2.node.arguments, fragmentSpread2.varMap)) { context.reportError(new GraphQLError(`Spreads "${fragmentSpread1.node.name.value}" conflict because ${fragmentSpread1.key} and ${fragmentSpread2.key} have different fragment arguments.`, { nodes: [fragmentSpread1.node, fragmentSpread2.node] })); return; } } if (comparedFragmentPairs.has(fragmentSpread1.key, fragmentSpread2.key, areMutuallyExclusive)) { return; } comparedFragmentPairs.add(fragmentSpread1.key, fragmentSpread2.key, areMutuallyExclusive); const fragment1 = context.getFragment(fragmentSpread1.node.name.value); const fragment2 = context.getFragment(fragmentSpread2.node.name.value); if (!fragment1 || !fragment2) { return; } const [fieldMap1, referencedFragmentSpreads1] = getReferencedFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, fragment1, fragmentSpread1.varMap); const [fieldMap2, referencedFragmentSpreads2] = getReferencedFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, fragment2, fragmentSpread2.varMap); collectConflictsBetween(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fieldMap1, fragmentSpread1.varMap, fieldMap2, fragmentSpread2.varMap); for (const referencedFragmentSpread2 of referencedFragmentSpreads2) { collectConflictsBetweenFragments(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fragmentSpread1, referencedFragmentSpread2); } for (const referencedFragmentSpread1 of referencedFragmentSpreads1) { collectConflictsBetweenFragments(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, referencedFragmentSpread1, fragmentSpread2); } } function findConflictsBetweenSubSelectionSets(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, parentType1, selectionSet1, varMap1, parentType2, selectionSet2, varMap2) { const conflicts = []; const [fieldMap1, fragmentSpreads1] = getFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, parentType1, selectionSet1, varMap1); const [fieldMap2, fragmentSpreads2] = getFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, parentType2, selectionSet2, varMap2); collectConflictsBetween(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fieldMap1, varMap1, fieldMap2, varMap2); for (const fragmentSpread2 of fragmentSpreads2) { collectConflictsBetweenFieldsAndFragment(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fieldMap1, fragmentSpread2); } for (const fragmentSpread1 of fragmentSpreads1) { collectConflictsBetweenFieldsAndFragment(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fieldMap2, fragmentSpread1); } for (const fragmentSpread1 of fragmentSpreads1) { for (const fragmentSpread2 of fragmentSpreads2) { collectConflictsBetweenFragments(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, fragmentSpread1, fragmentSpread2); } } return conflicts; } function collectConflictsWithin(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, fieldMap) { for (const [responseName, fields] of fieldMap.entries()) { if (fields.length > 1) { for (let i = 0; i < fields.length; i++) { for (let j = i + 1; j < fields.length; j++) { const conflict = findConflict(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, false, responseName, fields[i], undefined, fields[j], undefined); if (conflict) { conflicts.push(conflict); } } } } } } function collectConflictsBetween(context, conflicts, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, parentFieldsAreMutuallyExclusive, fieldMap1, varMap1, fieldMap2, varMap2) { for (const [responseName, fields1] of fieldMap1.entries()) { const fields2 = fieldMap2.get(responseName); if (fields2 != null) { for (const field1 of fields1) { for (const field2 of fields2) { const conflict = findConflict(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, parentFieldsAreMutuallyExclusive, responseName, field1, varMap1, field2, varMap2); if (conflict) { conflicts.push(conflict); } } } } } } function findConflict(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, parentFieldsAreMutuallyExclusive, responseName, field1, varMap1, field2, varMap2) { const [parentType1, node1, def1] = field1; const [parentType2, node2, def2] = field2; const areMutuallyExclusive = parentFieldsAreMutuallyExclusive || (parentType1 !== parentType2 && isObjectType(parentType1) && isObjectType(parentType2)); if (!areMutuallyExclusive) { const name1 = node1.name.value; const name2 = node2.name.value; if (name1 !== name2) { return [ [responseName, `"${name1}" and "${name2}" are different fields`], [node1], [node2], ]; } if (!sameArguments(node1.arguments, varMap1, node2.arguments, varMap2)) { return [ [responseName, 'they have differing arguments'], [node1], [node2], ]; } } const directives1 = node1.directives; const directives2 = node2.directives; const overlappingStreamReason = hasNoOverlappingStreams(directives1, varMap1, directives2, varMap2); if (overlappingStreamReason !== undefined) { return [[responseName, overlappingStreamReason], [node1], [node2]]; } const type1 = def1?.type; const type2 = def2?.type; if (type1 && type2 && doTypesConflict(type1, type2)) { return [ [ responseName, `they return conflicting types "${inspect(type1)}" and "${inspect(type2)}"`, ], [node1], [node2], ]; } const selectionSet1 = node1.selectionSet; const selectionSet2 = node2.selectionSet; if (selectionSet1 && selectionSet2) { const conflicts = findConflictsBetweenSubSelectionSets(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, getNamedType(type1), selectionSet1, varMap1, getNamedType(type2), selectionSet2, varMap2); return subfieldConflicts(conflicts, responseName, node1, node2); } } function sameArguments(args1, varMap1, args2, varMap2) { if (args1 === undefined || args1.length === 0) { return args2 === undefined || args2.length === 0; } if (args2 === undefined || args2.length === 0) { return false; } if (args1.length !== args2.length) { return false; } const values2 = new Map(args2.map(({ name, value }) => [ name.value, varMap2 === undefined ? value : replaceFragmentVariables(value, varMap2), ])); return args1.every((arg1) => { let value1 = arg1.value; if (varMap1) { value1 = replaceFragmentVariables(value1, varMap1); } const value2 = values2.get(arg1.name.value); if (value2 === undefined) { return false; } return stringifyValue(value1) === stringifyValue(value2); }); } function replaceFragmentVariables(valueNode, varMap) { switch (valueNode.kind) { case Kind.VARIABLE: return varMap.get(valueNode.name.value) ?? valueNode; case Kind.LIST: return { ...valueNode, values: valueNode.values.map((node) => replaceFragmentVariables(node, varMap)), }; case Kind.OBJECT: return { ...valueNode, fields: valueNode.fields.map((field) => ({ ...field, value: replaceFragmentVariables(field.value, varMap), })), }; default: { return valueNode; } } } function stringifyValue(value) { return print(sortValueNode(value)); } function getStreamDirective(directives) { return directives?.find((directive) => directive.name.value === 'stream'); } function hasNoOverlappingStreams(directives1, varMap1, directives2, varMap2) { const stream1 = getStreamDirective(directives1); const stream2 = getStreamDirective(directives2); if (!stream1 && !stream2) { return; } else if (stream1 && stream2) { if (sameArguments(stream1.arguments, varMap1, stream2.arguments, varMap2)) { return 'they have overlapping stream directives. See https://github.com/graphql/defer-stream-wg/discussions/100'; } return 'they have overlapping stream directives'; } return 'they have overlapping stream directives'; } function doTypesConflict(type1, type2) { if (isListType(type1)) { return isListType(type2) ? doTypesConflict(type1.ofType, type2.ofType) : true; } if (isListType(type2)) { return true; } if (isNonNullType(type1)) { return isNonNullType(type2) ? doTypesConflict(type1.ofType, type2.ofType) : true; } if (isNonNullType(type2)) { return true; } if (isLeafType(type1) || isLeafType(type2)) { return type1 !== type2; } return false; } function getFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, parentType, selectionSet, varMap) { const cached = cachedFieldsAndFragmentSpreads.get(selectionSet); if (cached) { return cached; } const nodeAndDefs = new Map(); const fragmentSpreads = new Map(); _collectFieldsAndFragmentSpreads(context, parentType, selectionSet, nodeAndDefs, fragmentSpreads, varMap); const result = [ nodeAndDefs, Array.from(fragmentSpreads.values()), ]; cachedFieldsAndFragmentSpreads.set(selectionSet, result); return result; } function getReferencedFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, fragment, varMap) { const cached = cachedFieldsAndFragmentSpreads.get(fragment.selectionSet); if (cached) { return cached; } const fragmentType = typeFromAST(context.getSchema(), fragment.typeCondition); return getFieldsAndFragmentSpreads(context, cachedFieldsAndFragmentSpreads, fragmentType, fragment.selectionSet, varMap); } function _collectFieldsAndFragmentSpreads(context, parentType, selectionSet, nodeAndDefs, fragmentSpreads, varMap) { for (const selection of selectionSet.selections) { switch (selection.kind) { case Kind.FIELD: { const fieldName = selection.name.value; let fieldDef; if (isObjectType(parentType) || isInterfaceType(parentType)) { fieldDef = parentType.getFields()[fieldName]; } const responseName = selection.alias ? selection.alias.value : fieldName; let nodeAndDefsList = nodeAndDefs.get(responseName); if (nodeAndDefsList == null) { nodeAndDefsList = []; nodeAndDefs.set(responseName, nodeAndDefsList); } nodeAndDefsList.push([parentType, selection, fieldDef]); break; } case Kind.FRAGMENT_SPREAD: { const fragmentSpread = getFragmentSpread(context, selection, varMap); fragmentSpreads.set(fragmentSpread.key, fragmentSpread); break; } case Kind.INLINE_FRAGMENT: { const typeCondition = selection.typeCondition; const inlineFragmentType = typeCondition ? typeFromAST(context.getSchema(), typeCondition) : parentType; _collectFieldsAndFragmentSpreads(context, inlineFragmentType, selection.selectionSet, nodeAndDefs, fragmentSpreads, varMap); break; } } } } function getFragmentSpread(context, fragmentSpreadNode, varMap) { let key = ''; const newVarMap = new Map(); const fragmentSignature = context.getFragmentSignatureByName()(fragmentSpreadNode.name.value); const argMap = new Map(); if (fragmentSpreadNode.arguments) { for (const arg of fragmentSpreadNode.arguments) { argMap.set(arg.name.value, arg.value); } } if (fragmentSignature?.variableDefinitions) { key += fragmentSpreadNode.name.value + '('; for (const [varName, variable] of fragmentSignature.variableDefinitions) { const value = argMap.get(varName); if (value) { key += varName + ': ' + print(sortValueNode(value)); } const arg = argMap.get(varName); if (arg !== undefined) { newVarMap.set(varName, varMap !== undefined ? replaceFragmentVariables(arg, varMap) : arg); } else if (variable.defaultValue) { newVarMap.set(varName, variable.defaultValue); } } key += ')'; } return { key, node: fragmentSpreadNode, varMap: newVarMap.size > 0 ? newVarMap : undefined, }; } function subfieldConflicts(conflicts, responseName, node1, node2) { if (conflicts.length > 0) { return [ [responseName, conflicts.map(([reason]) => reason)], [node1, ...conflicts.map(([, fields1]) => fields1).flat()], [node2, ...conflicts.map(([, , fields2]) => fields2).flat()], ]; } } class OrderedPairSet { constructor() { this._data = new Map(); } has(a, b, weaklyPresent) { const result = this._data.get(a)?.get(b); if (result === undefined) { return false; } return weaklyPresent ? true : weaklyPresent === result; } add(a, b, weaklyPresent) { const map = this._data.get(a); if (map === undefined) { this._data.set(a, new Map([[b, weaklyPresent]])); } else { map.set(b, weaklyPresent); } } } class PairSet { constructor() { this._orderedPairSet = new OrderedPairSet(); } has(a, b, weaklyPresent) { return a < b ? this._orderedPairSet.has(a, b, weaklyPresent) : this._orderedPairSet.has(b, a, weaklyPresent); } add(a, b, weaklyPresent) { if (a < b) { this._orderedPairSet.add(a, b, weaklyPresent); } else { this._orderedPairSet.add(b, a, weaklyPresent); } } } //# sourceMappingURL=OverlappingFieldsCanBeMergedRule.js.map