UNPKG

graphql

Version:

A Query Language and Runtime which can target any service.

422 lines 20.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OverlappingFieldsCanBeMergedRule = OverlappingFieldsCanBeMergedRule; const inspect_ts_1 = require("../../jsutils/inspect.js"); const GraphQLError_ts_1 = require("../../error/GraphQLError.js"); const kinds_ts_1 = require("../../language/kinds.js"); const printer_ts_1 = require("../../language/printer.js"); const definition_ts_1 = require("../../type/definition.js"); const sortValueNode_ts_1 = require("../../utilities/sortValueNode.js"); const typeFromAST_ts_1 = require("../../utilities/typeFromAST.js"); function reasonMessage(reason) { if (Array.isArray(reason)) { return reason .map(([responseName, subReason]) => `subfields "${responseName}" conflict because ` + reasonMessage(subReason)) .join(' and '); } return reason; } 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_ts_1.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_ts_1.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 && (0, definition_ts_1.isObjectType)(parentType1) && (0, definition_ts_1.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 "${(0, inspect_ts_1.inspect)(type1)}" and "${(0, inspect_ts_1.inspect)(type2)}"`, ], [node1], [node2], ]; } const selectionSet1 = node1.selectionSet; const selectionSet2 = node2.selectionSet; if (selectionSet1 && selectionSet2) { const conflicts = findConflictsBetweenSubSelectionSets(context, cachedFieldsAndFragmentSpreads, comparedFieldsAndFragmentPairs, comparedFragmentPairs, areMutuallyExclusive, (0, definition_ts_1.getNamedType)(type1), selectionSet1, varMap1, (0, definition_ts_1.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 kinds_ts_1.Kind.VARIABLE: return varMap.get(valueNode.name.value) ?? valueNode; case kinds_ts_1.Kind.LIST: return { ...valueNode, values: valueNode.values.map((node) => replaceFragmentVariables(node, varMap)), }; case kinds_ts_1.Kind.OBJECT: return { ...valueNode, fields: valueNode.fields.map((field) => ({ ...field, value: replaceFragmentVariables(field.value, varMap), })), }; default: { return valueNode; } } } function stringifyValue(value) { return (0, printer_ts_1.print)((0, sortValueNode_ts_1.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 ((0, definition_ts_1.isListType)(type1)) { return (0, definition_ts_1.isListType)(type2) ? doTypesConflict(type1.ofType, type2.ofType) : true; } if ((0, definition_ts_1.isListType)(type2)) { return true; } if ((0, definition_ts_1.isNonNullType)(type1)) { return (0, definition_ts_1.isNonNullType)(type2) ? doTypesConflict(type1.ofType, type2.ofType) : true; } if ((0, definition_ts_1.isNonNullType)(type2)) { return true; } if ((0, definition_ts_1.isLeafType)(type1) || (0, definition_ts_1.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 = (0, typeFromAST_ts_1.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 kinds_ts_1.Kind.FIELD: { const fieldName = selection.name.value; let fieldDef; if ((0, definition_ts_1.isObjectType)(parentType) || (0, definition_ts_1.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 kinds_ts_1.Kind.FRAGMENT_SPREAD: { const fragmentSpread = getFragmentSpread(context, selection, varMap); fragmentSpreads.set(fragmentSpread.key, fragmentSpread); break; } case kinds_ts_1.Kind.INLINE_FRAGMENT: { const typeCondition = selection.typeCondition; const inlineFragmentType = typeCondition ? (0, typeFromAST_ts_1.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 + ': ' + (0, printer_ts_1.print)((0, sortValueNode_ts_1.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