All Downloads are FREE. Search and download functionalities are using the official Maven repository.

node_modules.graphql.validation.rules.OverlappingFieldsCanBeMerged.js.flow Maven / Gradle / Ivy

There is a newer version: 1.1.2
Show newest version
/* @flow */
/**
 *  Copyright (c) 2015, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

import type { ValidationContext } from '../index';
import { GraphQLError } from '../../error';
import find from '../../jsutils/find';
import type {
  SelectionSetNode,
  FieldNode,
  ArgumentNode,
  FragmentDefinitionNode,
} from '../../language/ast';
import * as Kind from '../../language/kinds';
import { print } from '../../language/printer';
import {
  getNamedType,
  isLeafType,
  GraphQLList,
  GraphQLNonNull,
  GraphQLObjectType,
  GraphQLInterfaceType,
} from '../../type/definition';
import type {
  GraphQLNamedType,
  GraphQLOutputType,
  GraphQLCompositeType,
  GraphQLField
} from '../../type/definition';
import { typeFromAST } from '../../utilities/typeFromAST';


export function fieldsConflictMessage(
  responseName: string,
  reason: ConflictReasonMessage
): string {
  return `Fields "${responseName}" conflict because ${reasonMessage(reason)}` +
    '. Use different aliases on the fields to fetch both if this was ' +
    'intentional.';
}

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 OverlappingFieldsCanBeMerged(context: ValidationContext): any {
  // 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 comparedFragments = 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,
        comparedFragments,
        context.getParentType(),
        selectionSet
      );
      conflicts.forEach(
        ([ [ responseName, reason ], fields1, fields2 ]) =>
          context.reportError(new GraphQLError(
            fieldsConflictMessage(responseName, reason),
            fields1.concat(fields2)
          ))
      );
    }
  };
}

type Conflict = [ ConflictReason, Array, Array ];
// Field name and reason.
type ConflictReason = [ string, ConflictReasonMessage ];
// Reason is a string, or a nested list of conflicts.
type ConflictReasonMessage = string | Array;
// Tuple defining a field node in a context.
type NodeAndDef = [ GraphQLCompositeType, FieldNode, ?GraphQLField<*, *> ];
// Map of array of those.
type NodeAndDefCollection = { [key: string]: Array };

/**
 * 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,
  comparedFragments: PairSet,
  parentType: ?GraphQLNamedType,
  selectionSet: SelectionSetNode
): Array {
  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,
    comparedFragments,
    fieldMap
  );

  // (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,
      comparedFragments,
      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,
        comparedFragments,
        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,
  cachedFieldsAndFragmentNames,
  comparedFragments: PairSet,
  areMutuallyExclusive: boolean,
  fieldMap: NodeAndDefCollection,
  fragmentName: string
): void {
  const fragment = context.getFragment(fragmentName);
  if (!fragment) {
    return;
  }

  const [ fieldMap2, fragmentNames2 ] = getReferencedFieldsAndFragmentNames(
    context,
    cachedFieldsAndFragmentNames,
    fragment
  );

  // (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,
    comparedFragments,
    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,
      comparedFragments,
      areMutuallyExclusive,
      fieldMap,
      fragmentNames2[i]
    );
  }
}

// Collect all conflicts found between two fragments, including via spreading in
// any nested fragments.
function collectConflictsBetweenFragments(
  context: ValidationContext,
  conflicts: Array,
  cachedFieldsAndFragmentNames,
  comparedFragments: PairSet,
  areMutuallyExclusive: boolean,
  fragmentName1: string,
  fragmentName2: string
): void {
  const fragment1 = context.getFragment(fragmentName1);
  const fragment2 = context.getFragment(fragmentName2);
  if (!fragment1 || !fragment2) {
    return;
  }

  // No need to compare a fragment to itself.
  if (fragment1 === fragment2) {
    return;
  }

  // Memoize so two fragments are not compared for conflicts more than once.
  if (
    comparedFragments.has(fragmentName1, fragmentName2, areMutuallyExclusive)
  ) {
    return;
  }
  comparedFragments.add(fragmentName1, fragmentName2, areMutuallyExclusive);

  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,
    comparedFragments,
    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,
      comparedFragments,
      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,
      comparedFragments,
      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,
  comparedFragments: PairSet,
  areMutuallyExclusive: boolean,
  parentType1: ?GraphQLNamedType,
  selectionSet1: SelectionSetNode,
  parentType2: ?GraphQLNamedType,
  selectionSet2: SelectionSetNode
): Array {
  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,
    comparedFragments,
    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 (let j = 0; j < fragmentNames2.length; j++) {
    collectConflictsBetweenFieldsAndFragment(
      context,
      conflicts,
      cachedFieldsAndFragmentNames,
      comparedFragments,
      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.
  for (let i = 0; i < fragmentNames1.length; i++) {
    collectConflictsBetweenFieldsAndFragment(
      context,
      conflicts,
      cachedFieldsAndFragmentNames,
      comparedFragments,
      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,
        comparedFragments,
        areMutuallyExclusive,
        fragmentNames1[i],
        fragmentNames2[j]
      );
    }
  }
  return conflicts;
}

// Collect all Conflicts "within" one collection of fields.
function collectConflictsWithin(
  context: ValidationContext,
  conflicts: Array,
  cachedFieldsAndFragmentNames,
  comparedFragments: 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.
  Object.keys(fieldMap).forEach(responseName => {
    const fields = fieldMap[responseName];
    // 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,
            comparedFragments,
            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,
  cachedFieldsAndFragmentNames,
  comparedFragments: 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.
  Object.keys(fieldMap1).forEach(responseName => {
    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,
            comparedFragments,
            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,
  comparedFragments: 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 &&
    parentType1 instanceof GraphQLObjectType &&
    parentType2 instanceof GraphQLObjectType;

  // The return type for each field.
  const type1 = def1 && def1.type;
  const type2 = def2 && def2.type;

  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.arguments || [], node2.arguments || [])) {
      return [
        [ responseName, 'they have differing arguments' ],
        [ node1 ],
        [ node2 ]
      ];
    }
  }

  if (type1 && type2 && doTypesConflict(type1, type2)) {
    return [
      [ responseName,
        `they return conflicting types ${String(type1)} and ${String(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,
      comparedFragments,
      areMutuallyExclusive,
      getNamedType(type1),
      selectionSet1,
      getNamedType(type2),
      selectionSet2
    );
    return subfieldConflicts(conflicts, responseName, node1, node2);
  }
}

function sameArguments(
  arguments1: Array,
  arguments2: Array
): 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 (!value1 && !value2) || 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 (type1 instanceof GraphQLList) {
    return type2 instanceof GraphQLList ?
      doTypesConflict(type1.ofType, type2.ofType) :
      true;
  }
  if (type2 instanceof GraphQLList) {
    return type1 instanceof GraphQLList ?
      doTypesConflict(type1.ofType, type2.ofType) :
      true;
  }
  if (type1 instanceof GraphQLNonNull) {
    return type2 instanceof GraphQLNonNull ?
      doTypesConflict(type1.ofType, type2.ofType) :
      true;
  }
  if (type2 instanceof GraphQLNonNull) {
    return type1 instanceof GraphQLNonNull ?
      doTypesConflict(type1.ofType, type2.ofType) :
      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 ] {
  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 (let i = 0; i < selectionSet.selections.length; i++) {
    const selection = selectionSet.selections[i];
    switch (selection.kind) {
      case Kind.FIELD:
        const fieldName = selection.name.value;
        let fieldDef;
        if (parentType instanceof GraphQLObjectType ||
            parentType instanceof GraphQLInterfaceType) {
          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: Array,
  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: {[a: string]: {[b: string]: 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;
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy