NoFragmentCyclesRule.mjs 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import { GraphQLError } from '../../error/GraphQLError.mjs';
  2. /**
  3. * No fragment cycles
  4. *
  5. * The graph of fragment spreads must not form any cycles including spreading itself.
  6. * Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data.
  7. *
  8. * See https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles
  9. */
  10. export function NoFragmentCyclesRule(context) {
  11. // Tracks already visited fragments to maintain O(N) and to ensure that cycles
  12. // are not redundantly reported.
  13. const visitedFrags = Object.create(null); // Array of AST nodes used to produce meaningful errors
  14. const spreadPath = []; // Position in the spread path
  15. const spreadPathIndexByName = Object.create(null);
  16. return {
  17. OperationDefinition: () => false,
  18. FragmentDefinition(node) {
  19. detectCycleRecursive(node);
  20. return false;
  21. },
  22. }; // This does a straight-forward DFS to find cycles.
  23. // It does not terminate when a cycle was found but continues to explore
  24. // the graph to find all possible cycles.
  25. function detectCycleRecursive(fragment) {
  26. if (visitedFrags[fragment.name.value]) {
  27. return;
  28. }
  29. const fragmentName = fragment.name.value;
  30. visitedFrags[fragmentName] = true;
  31. const spreadNodes = context.getFragmentSpreads(fragment.selectionSet);
  32. if (spreadNodes.length === 0) {
  33. return;
  34. }
  35. spreadPathIndexByName[fragmentName] = spreadPath.length;
  36. for (const spreadNode of spreadNodes) {
  37. const spreadName = spreadNode.name.value;
  38. const cycleIndex = spreadPathIndexByName[spreadName];
  39. spreadPath.push(spreadNode);
  40. if (cycleIndex === undefined) {
  41. const spreadFragment = context.getFragment(spreadName);
  42. if (spreadFragment) {
  43. detectCycleRecursive(spreadFragment);
  44. }
  45. } else {
  46. const cyclePath = spreadPath.slice(cycleIndex);
  47. const viaPath = cyclePath
  48. .slice(0, -1)
  49. .map((s) => '"' + s.name.value + '"')
  50. .join(', ');
  51. context.reportError(
  52. new GraphQLError(
  53. `Cannot spread fragment "${spreadName}" within itself` +
  54. (viaPath !== '' ? ` via ${viaPath}.` : '.'),
  55. {
  56. nodes: cyclePath,
  57. },
  58. ),
  59. );
  60. }
  61. spreadPath.pop();
  62. }
  63. spreadPathIndexByName[fragmentName] = undefined;
  64. }
  65. }