You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
805 lines
24 KiB
805 lines
24 KiB
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; |
|
} |
|
/** |
|
* Overlapping fields can be merged |
|
* |
|
* A selection set is only valid if all fields (including spreading any |
|
* fragments) either correspond to distinct response names or can be merged |
|
* without ambiguity. |
|
* |
|
* See https://spec.graphql.org/draft/#sec-Field-Selection-Merging |
|
*/ |
|
|
|
export function OverlappingFieldsCanBeMergedRule(context) { |
|
// A memoization for when two fragments are compared "between" each other for |
|
// conflicts. Two fragments may be compared many times, so memoizing this can |
|
// dramatically improve the performance of this validator. |
|
const comparedFragmentPairs = new PairSet(); // A cache for the "field map" and list of fragment names found in any given |
|
// selection set. Selection sets may be asked for this information multiple |
|
// times, so this improves the performance of this validator. |
|
|
|
const cachedFieldsAndFragmentNames = new Map(); |
|
return { |
|
SelectionSet(selectionSet) { |
|
const conflicts = findConflictsWithinSelectionSet( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
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), |
|
}, |
|
), |
|
); |
|
} |
|
}, |
|
}; |
|
} |
|
|
|
/** |
|
* Algorithm: |
|
* |
|
* Conflicts occur when two fields exist in a query which will produce the same |
|
* response name, but represent differing values, thus creating a conflict. |
|
* The algorithm below finds all conflicts via making a series of comparisons |
|
* between fields. In order to compare as few fields as possible, this makes |
|
* a series of comparisons "within" sets of fields and "between" sets of fields. |
|
* |
|
* Given any selection set, a collection produces both a set of fields by |
|
* also including all inline fragments, as well as a list of fragments |
|
* referenced by fragment spreads. |
|
* |
|
* A) Each selection set represented in the document first compares "within" its |
|
* collected set of fields, finding any conflicts between every pair of |
|
* overlapping fields. |
|
* Note: This is the *only time* that a the fields "within" a set are compared |
|
* to each other. After this only fields "between" sets are compared. |
|
* |
|
* B) Also, if any fragment is referenced in a selection set, then a |
|
* comparison is made "between" the original set of fields and the |
|
* referenced fragment. |
|
* |
|
* C) Also, if multiple fragments are referenced, then comparisons |
|
* are made "between" each referenced fragment. |
|
* |
|
* D) When comparing "between" a set of fields and a referenced fragment, first |
|
* a comparison is made between each field in the original set of fields and |
|
* each field in the the referenced set of fields. |
|
* |
|
* E) Also, if any fragment is referenced in the referenced selection set, |
|
* then a comparison is made "between" the original set of fields and the |
|
* referenced fragment (recursively referring to step D). |
|
* |
|
* F) When comparing "between" two fragments, first a comparison is made between |
|
* each field in the first referenced set of fields and each field in the the |
|
* second referenced set of fields. |
|
* |
|
* G) Also, any fragments referenced by the first must be compared to the |
|
* second, and any fragments referenced by the second must be compared to the |
|
* first (recursively referring to step F). |
|
* |
|
* H) When comparing two fields, if both have selection sets, then a comparison |
|
* is made "between" both selection sets, first comparing the set of fields in |
|
* the first selection set with the set of fields in the second. |
|
* |
|
* I) Also, if any fragment is referenced in either selection set, then a |
|
* comparison is made "between" the other set of fields and the |
|
* referenced fragment. |
|
* |
|
* J) Also, if two fragments are referenced in both selection sets, then a |
|
* comparison is made "between" the two fragments. |
|
* |
|
*/ |
|
// Find all conflicts found "within" a selection set, including those found |
|
// via spreading in fragments. Called when visiting each SelectionSet in the |
|
// GraphQL Document. |
|
function findConflictsWithinSelectionSet( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
parentType, |
|
selectionSet, |
|
) { |
|
const conflicts = []; |
|
const [fieldMap, fragmentNames] = getFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
parentType, |
|
selectionSet, |
|
); // (A) Find find all conflicts "within" the fields of this selection set. |
|
// Note: this is the *only place* `collectConflictsWithin` is called. |
|
|
|
collectConflictsWithin( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
fieldMap, |
|
); |
|
|
|
if (fragmentNames.length !== 0) { |
|
// (B) Then collect conflicts between these fields and those represented by |
|
// each spread fragment name found. |
|
for (let i = 0; i < fragmentNames.length; i++) { |
|
collectConflictsBetweenFieldsAndFragment( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
false, |
|
fieldMap, |
|
fragmentNames[i], |
|
); // (C) Then compare this fragment with all other fragments found in this |
|
// selection set to collect conflicts between fragments spread together. |
|
// This compares each item in the list of fragment names to every other |
|
// item in that same list (except for itself). |
|
|
|
for (let j = i + 1; j < fragmentNames.length; j++) { |
|
collectConflictsBetweenFragments( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
false, |
|
fragmentNames[i], |
|
fragmentNames[j], |
|
); |
|
} |
|
} |
|
} |
|
|
|
return conflicts; |
|
} // Collect all conflicts found between a set of fields and a fragment reference |
|
// including via spreading in any nested fragments. |
|
|
|
function collectConflictsBetweenFieldsAndFragment( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fieldMap, |
|
fragmentName, |
|
) { |
|
const fragment = context.getFragment(fragmentName); |
|
|
|
if (!fragment) { |
|
return; |
|
} |
|
|
|
const [fieldMap2, referencedFragmentNames] = |
|
getReferencedFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
fragment, |
|
); // Do not compare a fragment's fieldMap to itself. |
|
|
|
if (fieldMap === fieldMap2) { |
|
return; |
|
} // (D) First collect any conflicts between the provided collection of fields |
|
// and the collection of fields represented by the given fragment. |
|
|
|
collectConflictsBetween( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fieldMap, |
|
fieldMap2, |
|
); // (E) Then collect any conflicts between the provided collection of fields |
|
// and any fragment names found in the given fragment. |
|
|
|
for (const referencedFragmentName of referencedFragmentNames) { |
|
// Memoize so two fragments are not compared for conflicts more than once. |
|
if ( |
|
comparedFragmentPairs.has( |
|
referencedFragmentName, |
|
fragmentName, |
|
areMutuallyExclusive, |
|
) |
|
) { |
|
continue; |
|
} |
|
|
|
comparedFragmentPairs.add( |
|
referencedFragmentName, |
|
fragmentName, |
|
areMutuallyExclusive, |
|
); |
|
collectConflictsBetweenFieldsAndFragment( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fieldMap, |
|
referencedFragmentName, |
|
); |
|
} |
|
} // Collect all conflicts found between two fragments, including via spreading in |
|
// any nested fragments. |
|
|
|
function collectConflictsBetweenFragments( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fragmentName1, |
|
fragmentName2, |
|
) { |
|
// No need to compare a fragment to itself. |
|
if (fragmentName1 === fragmentName2) { |
|
return; |
|
} // Memoize so two fragments are not compared for conflicts more than once. |
|
|
|
if ( |
|
comparedFragmentPairs.has( |
|
fragmentName1, |
|
fragmentName2, |
|
areMutuallyExclusive, |
|
) |
|
) { |
|
return; |
|
} |
|
|
|
comparedFragmentPairs.add(fragmentName1, fragmentName2, areMutuallyExclusive); |
|
const fragment1 = context.getFragment(fragmentName1); |
|
const fragment2 = context.getFragment(fragmentName2); |
|
|
|
if (!fragment1 || !fragment2) { |
|
return; |
|
} |
|
|
|
const [fieldMap1, referencedFragmentNames1] = |
|
getReferencedFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
fragment1, |
|
); |
|
const [fieldMap2, referencedFragmentNames2] = |
|
getReferencedFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
fragment2, |
|
); // (F) First, collect all conflicts between these two collections of fields |
|
// (not including any nested fragments). |
|
|
|
collectConflictsBetween( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fieldMap1, |
|
fieldMap2, |
|
); // (G) Then collect conflicts between the first fragment and any nested |
|
// fragments spread in the second fragment. |
|
|
|
for (const referencedFragmentName2 of referencedFragmentNames2) { |
|
collectConflictsBetweenFragments( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fragmentName1, |
|
referencedFragmentName2, |
|
); |
|
} // (G) Then collect conflicts between the second fragment and any nested |
|
// fragments spread in the first fragment. |
|
|
|
for (const referencedFragmentName1 of referencedFragmentNames1) { |
|
collectConflictsBetweenFragments( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
referencedFragmentName1, |
|
fragmentName2, |
|
); |
|
} |
|
} // Find all conflicts found between two selection sets, including those found |
|
// via spreading in fragments. Called when determining if conflicts exist |
|
// between the sub-fields of two overlapping fields. |
|
|
|
function findConflictsBetweenSubSelectionSets( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
parentType1, |
|
selectionSet1, |
|
parentType2, |
|
selectionSet2, |
|
) { |
|
const conflicts = []; |
|
const [fieldMap1, fragmentNames1] = getFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
parentType1, |
|
selectionSet1, |
|
); |
|
const [fieldMap2, fragmentNames2] = getFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
parentType2, |
|
selectionSet2, |
|
); // (H) First, collect all conflicts between these two collections of field. |
|
|
|
collectConflictsBetween( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fieldMap1, |
|
fieldMap2, |
|
); // (I) Then collect conflicts between the first collection of fields and |
|
// those referenced by each fragment name associated with the second. |
|
|
|
for (const fragmentName2 of fragmentNames2) { |
|
collectConflictsBetweenFieldsAndFragment( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fieldMap1, |
|
fragmentName2, |
|
); |
|
} // (I) Then collect conflicts between the second collection of fields and |
|
// those referenced by each fragment name associated with the first. |
|
|
|
for (const fragmentName1 of fragmentNames1) { |
|
collectConflictsBetweenFieldsAndFragment( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fieldMap2, |
|
fragmentName1, |
|
); |
|
} // (J) Also collect conflicts between any fragment names by the first and |
|
// fragment names by the second. This compares each item in the first set of |
|
// names to each item in the second set of names. |
|
|
|
for (const fragmentName1 of fragmentNames1) { |
|
for (const fragmentName2 of fragmentNames2) { |
|
collectConflictsBetweenFragments( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
fragmentName1, |
|
fragmentName2, |
|
); |
|
} |
|
} |
|
|
|
return conflicts; |
|
} // Collect all Conflicts "within" one collection of fields. |
|
|
|
function collectConflictsWithin( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
fieldMap, |
|
) { |
|
// A field map is a keyed collection, where each key represents a response |
|
// name and the value at that key is a list of all fields which provide that |
|
// response name. For every response name, if there are multiple fields, they |
|
// must be compared to find a potential conflict. |
|
for (const [responseName, fields] of Object.entries(fieldMap)) { |
|
// This compares every field in the list to every other field in this list |
|
// (except to itself). If the list only has one item, nothing needs to |
|
// be compared. |
|
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, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
false, // within one collection is never mutually exclusive |
|
responseName, |
|
fields[i], |
|
fields[j], |
|
); |
|
|
|
if (conflict) { |
|
conflicts.push(conflict); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} // Collect all Conflicts between two collections of fields. This is similar to, |
|
// but different from the `collectConflictsWithin` function above. This check |
|
// assumes that `collectConflictsWithin` has already been called on each |
|
// provided collection of fields. This is true because this validator traverses |
|
// each individual selection set. |
|
|
|
function collectConflictsBetween( |
|
context, |
|
conflicts, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
parentFieldsAreMutuallyExclusive, |
|
fieldMap1, |
|
fieldMap2, |
|
) { |
|
// A field map is a keyed collection, where each key represents a response |
|
// name and the value at that key is a list of all fields which provide that |
|
// response name. For any response name which appears in both provided field |
|
// maps, each field from the first field map must be compared to every field |
|
// in the second field map to find potential conflicts. |
|
for (const [responseName, fields1] of Object.entries(fieldMap1)) { |
|
const fields2 = fieldMap2[responseName]; |
|
|
|
if (fields2) { |
|
for (const field1 of fields1) { |
|
for (const field2 of fields2) { |
|
const conflict = findConflict( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
parentFieldsAreMutuallyExclusive, |
|
responseName, |
|
field1, |
|
field2, |
|
); |
|
|
|
if (conflict) { |
|
conflicts.push(conflict); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} // Determines if there is a conflict between two particular fields, including |
|
// comparing their sub-fields. |
|
|
|
function findConflict( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
parentFieldsAreMutuallyExclusive, |
|
responseName, |
|
field1, |
|
field2, |
|
) { |
|
const [parentType1, node1, def1] = field1; |
|
const [parentType2, node2, def2] = field2; // If it is known that two fields could not possibly apply at the same |
|
// time, due to the parent types, then it is safe to permit them to diverge |
|
// in aliased field or arguments used as they will not present any ambiguity |
|
// by differing. |
|
// It is known that two parent types could never overlap if they are |
|
// different Object types. Interface or Union types might overlap - if not |
|
// in the current state of the schema, then perhaps in some future version, |
|
// thus may not safely diverge. |
|
|
|
const areMutuallyExclusive = |
|
parentFieldsAreMutuallyExclusive || |
|
(parentType1 !== parentType2 && |
|
isObjectType(parentType1) && |
|
isObjectType(parentType2)); |
|
|
|
if (!areMutuallyExclusive) { |
|
// Two aliases must refer to the same field. |
|
const name1 = node1.name.value; |
|
const name2 = node2.name.value; |
|
|
|
if (name1 !== name2) { |
|
return [ |
|
[responseName, `"${name1}" and "${name2}" are different fields`], |
|
[node1], |
|
[node2], |
|
]; |
|
} // Two field calls must have the same arguments. |
|
|
|
if (!sameArguments(node1, node2)) { |
|
return [ |
|
[responseName, 'they have differing arguments'], |
|
[node1], |
|
[node2], |
|
]; |
|
} |
|
} // The return type for each field. |
|
|
|
const type1 = def1 === null || def1 === void 0 ? void 0 : def1.type; |
|
const type2 = def2 === null || def2 === void 0 ? void 0 : def2.type; |
|
|
|
if (type1 && type2 && doTypesConflict(type1, type2)) { |
|
return [ |
|
[ |
|
responseName, |
|
`they return conflicting types "${inspect(type1)}" and "${inspect( |
|
type2, |
|
)}"`, |
|
], |
|
[node1], |
|
[node2], |
|
]; |
|
} // Collect and compare sub-fields. Use the same "visited fragment names" list |
|
// for both collections so fields in a fragment reference are never |
|
// compared to themselves. |
|
|
|
const selectionSet1 = node1.selectionSet; |
|
const selectionSet2 = node2.selectionSet; |
|
|
|
if (selectionSet1 && selectionSet2) { |
|
const conflicts = findConflictsBetweenSubSelectionSets( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
comparedFragmentPairs, |
|
areMutuallyExclusive, |
|
getNamedType(type1), |
|
selectionSet1, |
|
getNamedType(type2), |
|
selectionSet2, |
|
); |
|
return subfieldConflicts(conflicts, responseName, node1, node2); |
|
} |
|
} |
|
|
|
function sameArguments(node1, node2) { |
|
const args1 = node1.arguments; |
|
const args2 = node2.arguments; |
|
|
|
if (args1 === undefined || args1.length === 0) { |
|
return args2 === undefined || args2.length === 0; |
|
} |
|
|
|
if (args2 === undefined || args2.length === 0) { |
|
return false; |
|
} |
|
/* c8 ignore next */ |
|
|
|
if (args1.length !== args2.length) { |
|
/* c8 ignore next */ |
|
return false; |
|
/* c8 ignore next */ |
|
} |
|
|
|
const values2 = new Map(args2.map(({ name, value }) => [name.value, value])); |
|
return args1.every((arg1) => { |
|
const value1 = arg1.value; |
|
const value2 = values2.get(arg1.name.value); |
|
|
|
if (value2 === undefined) { |
|
return false; |
|
} |
|
|
|
return stringifyValue(value1) === stringifyValue(value2); |
|
}); |
|
} |
|
|
|
function stringifyValue(value) { |
|
return print(sortValueNode(value)); |
|
} // Two types conflict if both types could not apply to a value simultaneously. |
|
// Composite types are ignored as their individual field types will be compared |
|
// later recursively. However List and Non-Null types must match. |
|
|
|
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; |
|
} // Given a selection set, return the collection of fields (a mapping of response |
|
// name to field nodes and definitions) as well as a list of fragment names |
|
// referenced via fragment spreads. |
|
|
|
function getFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
parentType, |
|
selectionSet, |
|
) { |
|
const cached = cachedFieldsAndFragmentNames.get(selectionSet); |
|
|
|
if (cached) { |
|
return cached; |
|
} |
|
|
|
const nodeAndDefs = Object.create(null); |
|
const fragmentNames = Object.create(null); |
|
|
|
_collectFieldsAndFragmentNames( |
|
context, |
|
parentType, |
|
selectionSet, |
|
nodeAndDefs, |
|
fragmentNames, |
|
); |
|
|
|
const result = [nodeAndDefs, Object.keys(fragmentNames)]; |
|
cachedFieldsAndFragmentNames.set(selectionSet, result); |
|
return result; |
|
} // Given a reference to a fragment, return the represented collection of fields |
|
// as well as a list of nested fragment names referenced via fragment spreads. |
|
|
|
function getReferencedFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
fragment, |
|
) { |
|
// Short-circuit building a type from the node if possible. |
|
const cached = cachedFieldsAndFragmentNames.get(fragment.selectionSet); |
|
|
|
if (cached) { |
|
return cached; |
|
} |
|
|
|
const fragmentType = typeFromAST(context.getSchema(), fragment.typeCondition); |
|
return getFieldsAndFragmentNames( |
|
context, |
|
cachedFieldsAndFragmentNames, |
|
fragmentType, |
|
fragment.selectionSet, |
|
); |
|
} |
|
|
|
function _collectFieldsAndFragmentNames( |
|
context, |
|
parentType, |
|
selectionSet, |
|
nodeAndDefs, |
|
fragmentNames, |
|
) { |
|
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; |
|
|
|
if (!nodeAndDefs[responseName]) { |
|
nodeAndDefs[responseName] = []; |
|
} |
|
|
|
nodeAndDefs[responseName].push([parentType, selection, fieldDef]); |
|
break; |
|
} |
|
|
|
case Kind.FRAGMENT_SPREAD: |
|
fragmentNames[selection.name.value] = true; |
|
break; |
|
|
|
case Kind.INLINE_FRAGMENT: { |
|
const typeCondition = selection.typeCondition; |
|
const inlineFragmentType = typeCondition |
|
? typeFromAST(context.getSchema(), typeCondition) |
|
: parentType; |
|
|
|
_collectFieldsAndFragmentNames( |
|
context, |
|
inlineFragmentType, |
|
selection.selectionSet, |
|
nodeAndDefs, |
|
fragmentNames, |
|
); |
|
|
|
break; |
|
} |
|
} |
|
} |
|
} // Given a series of Conflicts which occurred between two sub-fields, generate |
|
// a single Conflict. |
|
|
|
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()], |
|
]; |
|
} |
|
} |
|
/** |
|
* A way to keep track of pairs of things when the ordering of the pair does not matter. |
|
*/ |
|
|
|
class PairSet { |
|
constructor() { |
|
this._data = new Map(); |
|
} |
|
|
|
has(a, b, areMutuallyExclusive) { |
|
var _this$_data$get; |
|
|
|
const [key1, key2] = a < b ? [a, b] : [b, a]; |
|
const result = |
|
(_this$_data$get = this._data.get(key1)) === null || |
|
_this$_data$get === void 0 |
|
? void 0 |
|
: _this$_data$get.get(key2); |
|
|
|
if (result === undefined) { |
|
return false; |
|
} // areMutuallyExclusive being false is a superset of being true, hence if |
|
// we want to know if this PairSet "has" these two with no exclusivity, |
|
// we have to ensure it was added as such. |
|
|
|
return areMutuallyExclusive ? true : areMutuallyExclusive === result; |
|
} |
|
|
|
add(a, b, areMutuallyExclusive) { |
|
const [key1, key2] = a < b ? [a, b] : [b, a]; |
|
|
|
const map = this._data.get(key1); |
|
|
|
if (map === undefined) { |
|
this._data.set(key1, new Map([[key2, areMutuallyExclusive]])); |
|
} else { |
|
map.set(key2, areMutuallyExclusive); |
|
} |
|
} |
|
}
|
|
|