Spaces:
Running
Running
import { GraphQLError } from '../../error/GraphQLError.mjs'; | |
/** | |
* No fragment cycles | |
* | |
* The graph of fragment spreads must not form any cycles including spreading itself. | |
* Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data. | |
* | |
* See https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles | |
*/ | |
export function NoFragmentCyclesRule(context) { | |
// Tracks already visited fragments to maintain O(N) and to ensure that cycles | |
// are not redundantly reported. | |
const visitedFrags = Object.create(null); // Array of AST nodes used to produce meaningful errors | |
const spreadPath = []; // Position in the spread path | |
const spreadPathIndexByName = Object.create(null); | |
return { | |
OperationDefinition: () => false, | |
FragmentDefinition(node) { | |
detectCycleRecursive(node); | |
return false; | |
}, | |
}; // This does a straight-forward DFS to find cycles. | |
// It does not terminate when a cycle was found but continues to explore | |
// the graph to find all possible cycles. | |
function detectCycleRecursive(fragment) { | |
if (visitedFrags[fragment.name.value]) { | |
return; | |
} | |
const fragmentName = fragment.name.value; | |
visitedFrags[fragmentName] = true; | |
const spreadNodes = context.getFragmentSpreads(fragment.selectionSet); | |
if (spreadNodes.length === 0) { | |
return; | |
} | |
spreadPathIndexByName[fragmentName] = spreadPath.length; | |
for (const spreadNode of spreadNodes) { | |
const spreadName = spreadNode.name.value; | |
const cycleIndex = spreadPathIndexByName[spreadName]; | |
spreadPath.push(spreadNode); | |
if (cycleIndex === undefined) { | |
const spreadFragment = context.getFragment(spreadName); | |
if (spreadFragment) { | |
detectCycleRecursive(spreadFragment); | |
} | |
} else { | |
const cyclePath = spreadPath.slice(cycleIndex); | |
const viaPath = cyclePath | |
.slice(0, -1) | |
.map((s) => '"' + s.name.value + '"') | |
.join(', '); | |
context.reportError( | |
new GraphQLError( | |
`Cannot spread fragment "${spreadName}" within itself` + | |
(viaPath !== '' ? ` via ${viaPath}.` : '.'), | |
{ | |
nodes: cyclePath, | |
}, | |
), | |
); | |
} | |
spreadPath.pop(); | |
} | |
spreadPathIndexByName[fragmentName] = undefined; | |
} | |
} | |