| // @flow strict |
| |
| import find from '../../polyfills/find'; |
| import objectEntries from '../../polyfills/objectEntries'; |
| |
| import inspect from '../../jsutils/inspect'; |
| import { type ObjMap } from '../../jsutils/ObjMap'; |
| |
| import { GraphQLError } from '../../error/GraphQLError'; |
| |
| import { Kind } from '../../language/kinds'; |
| import { print } from '../../language/printer'; |
| import { type ASTVisitor } from '../../language/visitor'; |
| import { |
| type SelectionSetNode, |
| type FieldNode, |
| type ArgumentNode, |
| type FragmentDefinitionNode, |
| } from '../../language/ast'; |
| |
| import { |
| type GraphQLNamedType, |
| type GraphQLOutputType, |
| type GraphQLCompositeType, |
| type GraphQLField, |
| getNamedType, |
| isNonNullType, |
| isLeafType, |
| isObjectType, |
| isListType, |
| isInterfaceType, |
| } from '../../type/definition'; |
| |
| import { typeFromAST } from '../../utilities/typeFromAST'; |
| |
| import { type ValidationContext } from '../ValidationContext'; |
| |
| function reasonMessage(reason: ConflictReasonMessage): string { |
| 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. |
| */ |
| export function OverlappingFieldsCanBeMergedRule( |
| context: ValidationContext, |
| ): ASTVisitor { |
| // 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.`, |
| fields1.concat(fields2), |
| ), |
| ); |
| } |
| }, |
| }; |
| } |
| |
| type Conflict = [ConflictReason, Array<FieldNode>, Array<FieldNode>]; |
| // Field name and reason. |
| type ConflictReason = [string, ConflictReasonMessage]; |
| // Reason is a string, or a nested list of conflicts. |
| type ConflictReasonMessage = string | Array<ConflictReason>; |
| // Tuple defining a field node in a context. |
| type NodeAndDef = [ |
| GraphQLCompositeType, |
| FieldNode, |
| ?GraphQLField<mixed, mixed>, |
| ]; |
| // Map of array of those. |
| type NodeAndDefCollection = ObjMap<Array<NodeAndDef>>; |
| |
| /** |
| * 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: ValidationContext, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs: PairSet, |
| parentType: ?GraphQLNamedType, |
| selectionSet: SelectionSetNode, |
| ): Array<Conflict> { |
| 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: ValidationContext, |
| conflicts: Array<Conflict>, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs: PairSet, |
| areMutuallyExclusive: boolean, |
| fieldMap: NodeAndDefCollection, |
| fragmentName: string, |
| ): void { |
| const fragment = context.getFragment(fragmentName); |
| if (!fragment) { |
| return; |
| } |
| |
| const [fieldMap2, fragmentNames2] = 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 (let i = 0; i < fragmentNames2.length; i++) { |
| collectConflictsBetweenFieldsAndFragment( |
| context, |
| conflicts, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs, |
| areMutuallyExclusive, |
| fieldMap, |
| fragmentNames2[i], |
| ); |
| } |
| } |
| |
| // Collect all conflicts found between two fragments, including via spreading in |
| // any nested fragments. |
| function collectConflictsBetweenFragments( |
| context: ValidationContext, |
| conflicts: Array<Conflict>, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs: PairSet, |
| areMutuallyExclusive: boolean, |
| fragmentName1: string, |
| fragmentName2: string, |
| ): void { |
| // 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, fragmentNames1] = getReferencedFieldsAndFragmentNames( |
| context, |
| cachedFieldsAndFragmentNames, |
| fragment1, |
| ); |
| const [fieldMap2, fragmentNames2] = 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 (let j = 0; j < fragmentNames2.length; j++) { |
| collectConflictsBetweenFragments( |
| context, |
| conflicts, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs, |
| areMutuallyExclusive, |
| fragmentName1, |
| fragmentNames2[j], |
| ); |
| } |
| |
| // (G) Then collect conflicts between the second fragment and any nested |
| // fragments spread in the first fragment. |
| for (let i = 0; i < fragmentNames1.length; i++) { |
| collectConflictsBetweenFragments( |
| context, |
| conflicts, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs, |
| areMutuallyExclusive, |
| fragmentNames1[i], |
| 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: ValidationContext, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs: PairSet, |
| areMutuallyExclusive: boolean, |
| parentType1: ?GraphQLNamedType, |
| selectionSet1: SelectionSetNode, |
| parentType2: ?GraphQLNamedType, |
| selectionSet2: SelectionSetNode, |
| ): Array<Conflict> { |
| 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. |
| if (fragmentNames2.length !== 0) { |
| for (let j = 0; j < fragmentNames2.length; j++) { |
| collectConflictsBetweenFieldsAndFragment( |
| context, |
| conflicts, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs, |
| areMutuallyExclusive, |
| fieldMap1, |
| fragmentNames2[j], |
| ); |
| } |
| } |
| |
| // (I) Then collect conflicts between the second collection of fields and |
| // those referenced by each fragment name associated with the first. |
| if (fragmentNames1.length !== 0) { |
| for (let i = 0; i < fragmentNames1.length; i++) { |
| collectConflictsBetweenFieldsAndFragment( |
| context, |
| conflicts, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs, |
| areMutuallyExclusive, |
| fieldMap2, |
| fragmentNames1[i], |
| ); |
| } |
| } |
| |
| // (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 (let i = 0; i < fragmentNames1.length; i++) { |
| for (let j = 0; j < fragmentNames2.length; j++) { |
| collectConflictsBetweenFragments( |
| context, |
| conflicts, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs, |
| areMutuallyExclusive, |
| fragmentNames1[i], |
| fragmentNames2[j], |
| ); |
| } |
| } |
| return conflicts; |
| } |
| |
| // Collect all Conflicts "within" one collection of fields. |
| function collectConflictsWithin( |
| context: ValidationContext, |
| conflicts: Array<Conflict>, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs: PairSet, |
| fieldMap: NodeAndDefCollection, |
| ): void { |
| // 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 objectEntries(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: ValidationContext, |
| conflicts: Array<Conflict>, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs: PairSet, |
| parentFieldsAreMutuallyExclusive: boolean, |
| fieldMap1: NodeAndDefCollection, |
| fieldMap2: NodeAndDefCollection, |
| ): void { |
| // 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 of Object.keys(fieldMap1)) { |
| const fields2 = fieldMap2[responseName]; |
| if (fields2) { |
| const fields1 = fieldMap1[responseName]; |
| for (let i = 0; i < fields1.length; i++) { |
| for (let j = 0; j < fields2.length; j++) { |
| const conflict = findConflict( |
| context, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs, |
| parentFieldsAreMutuallyExclusive, |
| responseName, |
| fields1[i], |
| fields2[j], |
| ); |
| if (conflict) { |
| conflicts.push(conflict); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Determines if there is a conflict between two particular fields, including |
| // comparing their sub-fields. |
| function findConflict( |
| context: ValidationContext, |
| cachedFieldsAndFragmentNames, |
| comparedFragmentPairs: PairSet, |
| parentFieldsAreMutuallyExclusive: boolean, |
| responseName: string, |
| field1: NodeAndDef, |
| field2: NodeAndDef, |
| ): ?Conflict { |
| 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], |
| ]; |
| } |
| |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const args1 = node1.arguments ?? []; |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const args2 = node2.arguments ?? []; |
| // Two field calls must have the same arguments. |
| if (!sameArguments(args1, args2)) { |
| return [ |
| [responseName, 'they have differing arguments'], |
| [node1], |
| [node2], |
| ]; |
| } |
| } |
| |
| // The return type for each field. |
| 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], |
| ]; |
| } |
| |
| // 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( |
| arguments1: $ReadOnlyArray<ArgumentNode>, |
| arguments2: $ReadOnlyArray<ArgumentNode>, |
| ): boolean { |
| if (arguments1.length !== arguments2.length) { |
| return false; |
| } |
| return arguments1.every(argument1 => { |
| const argument2 = find( |
| arguments2, |
| argument => argument.name.value === argument1.name.value, |
| ); |
| if (!argument2) { |
| return false; |
| } |
| return sameValue(argument1.value, argument2.value); |
| }); |
| } |
| |
| function sameValue(value1, value2) { |
| return print(value1) === print(value2); |
| } |
| |
| // 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: GraphQLOutputType, |
| type2: GraphQLOutputType, |
| ): boolean { |
| 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: ValidationContext, |
| cachedFieldsAndFragmentNames, |
| parentType: ?GraphQLNamedType, |
| selectionSet: SelectionSetNode, |
| ): [NodeAndDefCollection, Array<string>] { |
| let cached = cachedFieldsAndFragmentNames.get(selectionSet); |
| if (!cached) { |
| const nodeAndDefs = Object.create(null); |
| const fragmentNames = Object.create(null); |
| _collectFieldsAndFragmentNames( |
| context, |
| parentType, |
| selectionSet, |
| nodeAndDefs, |
| fragmentNames, |
| ); |
| cached = [nodeAndDefs, Object.keys(fragmentNames)]; |
| cachedFieldsAndFragmentNames.set(selectionSet, cached); |
| } |
| return cached; |
| } |
| |
| // 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: ValidationContext, |
| cachedFieldsAndFragmentNames, |
| fragment: FragmentDefinitionNode, |
| ) { |
| // 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: ValidationContext, |
| parentType: ?GraphQLNamedType, |
| selectionSet: SelectionSetNode, |
| nodeAndDefs, |
| fragmentNames, |
| ): void { |
| 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: $ReadOnlyArray<Conflict>, |
| responseName: string, |
| node1: FieldNode, |
| node2: FieldNode, |
| ): ?Conflict { |
| if (conflicts.length > 0) { |
| return [ |
| [responseName, conflicts.map(([reason]) => reason)], |
| conflicts.reduce((allFields, [, fields1]) => allFields.concat(fields1), [ |
| node1, |
| ]), |
| conflicts.reduce( |
| (allFields, [, , fields2]) => allFields.concat(fields2), |
| [node2], |
| ), |
| ]; |
| } |
| } |
| |
| /** |
| * A way to keep track of pairs of things when the ordering of the pair does |
| * not matter. We do this by maintaining a sort of double adjacency sets. |
| */ |
| class PairSet { |
| _data: ObjMap<ObjMap<boolean>>; |
| |
| constructor(): void { |
| this._data = Object.create(null); |
| } |
| |
| has(a: string, b: string, areMutuallyExclusive: boolean) { |
| const first = this._data[a]; |
| const result = first && first[b]; |
| 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. |
| if (areMutuallyExclusive === false) { |
| return result === false; |
| } |
| return true; |
| } |
| |
| add(a: string, b: string, areMutuallyExclusive: boolean) { |
| _pairSetAdd(this._data, a, b, areMutuallyExclusive); |
| _pairSetAdd(this._data, b, a, areMutuallyExclusive); |
| } |
| } |
| |
| function _pairSetAdd(data, a, b, areMutuallyExclusive) { |
| let map = data[a]; |
| if (!map) { |
| map = Object.create(null); |
| data[a] = map; |
| } |
| map[b] = areMutuallyExclusive; |
| } |