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.
78 lines
2.3 KiB
78 lines
2.3 KiB
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; |
|
} |
|
}
|
|
|