feat(validation): reject directive definition cycles#4726
Conversation
|
@yaacovCR is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel. A member of the Team first needs to authorize it. |
|
Thank you for looking into this and implementing the validation @yaacovCR! About extensions: to me, and generally speaking, it would be surprising if a declaration that is invalid in the context of a schema could still be made valid via extensions. Unless this can already happen otherwise in the current spec, I would be in favor of avoiding that. Meaning: IMO it would make sense for the validation to also detect cycles introduced by extensions. I you agree to that, do you also think that the current wording in the proposed spec ( |
|
I’m not sure we actually need a change in wording, after reconsidering for a little bit. Although applied directors currently don’t exist, that could change in the future, and we need to preserve the option for directives or a subset of them to be preserved and therefore not cyclical. Will make changes! |
9494943 to
e66c36d
Compare
e66c36d to
45bb6e5
Compare
…l#4726 Adds a new `NoDirectiveDefinitionCyclesRule` SDL validation rule that detects cycles in the graph of directives used within directive definitions. A cycle arises when following the references within directive definitions leads back to the same directive. Two kinds of references are tracked: 1. **Directive argument types** — if `@foo` takes an argument of type `InputObject`, and `InputObject` has a field with directive `@foo`, that forms a cycle: `@foo → InputObject → @foo`. 2. **Directives on directive definitions** — when the experimental `experimentalDirectivesOnDirectiveDefinitions` parser option is enabled, a directive applied directly to a directive definition or extension is also a reference. If `@a` is annotated with `@b` and `@b` is annotated with `@a`, that is a cycle: `@a → @b → @a`. Cycles make it impossible to build a well-defined schema; the spec already [forbids them](https://spec.graphql.org/draft/#sec-Type-System.Directives.Type-Validation), and experimental directive extensions [do the same](https://github.com/graphql/graphql-spec/pull/1206/changes#diff-30a69c5a5eded8e1aea52e53dad1181e6ec8f549ca2c50570b035153e2de1c43R2342). perf(validation): prune directive cycle reference sources refactor(validation): collect directive cycle references with visitors fix(validation): include member references in directive cycle paths fix(validation): avoid masking directive cycles with memoization fix(validation): collect directive cycle references from schema objects perf(validation): skip directive cycle rule when no directives can cycle
…l#4726 Adds a new `NoDirectiveDefinitionCyclesRule` SDL validation rule that detects cycles in the graph of directives used within directive definitions. A cycle arises when following the references within directive definitions leads back to the same directive. Two kinds of references are tracked: 1. **Directive argument types** — if `@foo` takes an argument of type `InputObject`, and `InputObject` has a field with directive `@foo`, that forms a cycle: `@foo → InputObject → @foo`. 2. **Directives on directive definitions** — when the experimental `experimentalDirectivesOnDirectiveDefinitions` parser option is enabled, a directive applied directly to a directive definition or extension is also a reference. If `@a` is annotated with `@b` and `@b` is annotated with `@a`, that is a cycle: `@a → @b → @a`. Cycles make it impossible to build a well-defined schema; the spec already [forbids them](https://spec.graphql.org/draft/#sec-Type-System.Directives.Type-Validation), and experimental directive extensions [do the same](https://github.com/graphql/graphql-spec/pull/1206/changes#diff-30a69c5a5eded8e1aea52e53dad1181e6ec8f549ca2c50570b035153e2de1c43R2342).
## v17.0.0 (2026-06-15) #### New Feature 🚀 * [#4819](#4819) feat: graduate directives on directives ([@yaacovCR](https://github.com/yaacovCR)) #### Bug Fix 🐞 * [#4799](#4799) fix: raise request error on invalid fragment variables ([@yaacovCR](https://github.com/yaacovCR)) * [#4800](#4800) fix: apply directives when SDL contains type definitions and extensions with directives ([@yaacovCR](https://github.com/yaacovCR)) * [#4564](#4564) OneOf Inhabitability ([@jbellenger](https://github.com/jbellenger)) * [#4814](#4814) fix(KnownDirectivesRule): locations for input field arguments in extensions ([@yaacovCR](https://github.com/yaacovCR)) * [#4726](#4726) feat(validation): reject directive definition cycles ([@yaacovCR](https://github.com/yaacovCR)) * [#4815](#4815) Revert "feat(validation): reject directive definition cycles (#4726)" ([@yaacovCR](https://github.com/yaacovCR)) #### Docs 📝 <details> <summary> 11 PRs were merged </summary> * [#4790](#4790) docs: minor diagnostics doc comment improvements ([@yaacovCR](https://github.com/yaacovCR)) * [#4791](#4791) fix: docs: polish diagnostics comments further ([@yaacovCR](https://github.com/yaacovCR)) * [#4793](#4793) docs: further improve general execution and tracing docs ([@yaacovCR](https://github.com/yaacovCR)) * [#4802](#4802) docs: correct extension field comments - v17 ([@yaacovCR](https://github.com/yaacovCR)) * [#4805](#4805) docs: publish fixed extensions comments ([@yaacovCR](https://github.com/yaacovCR)) * [#4807](#4807) docs: add prettier for jsdoc examples ([@yaacovCR](https://github.com/yaacovCR)) * [#4435](#4435) Subscriptions docs suggestions ([@Urigo](https://github.com/Urigo)) * [#4811](#4811) docs: fix a few indentations inside string literals ([@yaacovCR](https://github.com/yaacovCR)) * [#4813](#4813) internal: docs update ([@yaacovCR](https://github.com/yaacovCR)) * [#4820](#4820) docs: document `@experimental_disableErrorPropagation` ([@yaacovCR](https://github.com/yaacovCR)) * [#4817](#4817) docs: post 17.rc-0 update ([@yaacovCR](https://github.com/yaacovCR)) </details> #### Polish 💅 <details> <summary> 2 PRs were merged </summary> * [#4809](#4809) internal: use prettier for non-generated website files ([@yaacovCR](https://github.com/yaacovCR)) * [#4812](#4812) polish: fix stream test cases ([@yaacovCR](https://github.com/yaacovCR)) </details> #### Internal 🏠 <details> <summary> 6 PRs were merged </summary> * [#4795](#4795) chore: move website publishing from 16.x.x to 17.x.x ([@yaacovCR](https://github.com/yaacovCR)) * [#4796](#4796) internal: fix broken npm/deno deployments ([@yaacovCR](https://github.com/yaacovCR)) * [#4797](#4797) ci: update GitHub Actions versions ([@yaacovCR](https://github.com/yaacovCR)) * [#4806](#4806) internal: update frontmatter ([@yaacovCR](https://github.com/yaacovCR)) * [#4808](#4808) intenral: fix ci badge ([@yaacovCR](https://github.com/yaacovCR)) * [#4810](#4810) internal: add prettier:examples to lint-staged ([@yaacovCR](https://github.com/yaacovCR)) </details> #### Committers: 3 * James Bellenger([@jbellenger](https://github.com/jbellenger)) * Uri Goldshtein([@Urigo](https://github.com/Urigo)) * Yaacov Rydzinski ([@yaacovCR](https://github.com/yaacovCR))
…l#4726 Adds a new `NoDirectiveDefinitionCyclesRule` SDL validation rule that detects cycles in the graph of directives used within directive definitions. A cycle arises when following the references within directive definitions leads back to the same directive. Two kinds of references are tracked: 1. **Directive argument types** — if `@foo` takes an argument of type `InputObject`, and `InputObject` has a field with directive `@foo`, that forms a cycle: `@foo → InputObject → @foo`. 2. **Directives on directive definitions** — when the experimental `experimentalDirectivesOnDirectiveDefinitions` parser option is enabled, a directive applied directly to a directive definition or extension is also a reference. If `@a` is annotated with `@b` and `@b` is annotated with `@a`, that is a cycle: `@a → @b → @a`. Cycles make it impossible to build a well-defined schema; the spec already [forbids them](https://spec.graphql.org/draft/#sec-Type-System.Directives.Type-Validation), and experimental directive extensions [do the same](https://github.com/graphql/graphql-spec/pull/1206/changes#diff-30a69c5a5eded8e1aea52e53dad1181e6ec8f549ca2c50570b035153e2de1c43R2342).
Adds a new
NoDirectiveDefinitionCyclesRuleSDL validation rule that detects cycles in the graph of directives used within directive definitions.What is a directive definition cycle?
A cycle arises when following the references within directive definitions leads back to the same directive. Two kinds of references are tracked:
@footakes an argument of typeInputObject, andInputObjecthas a field with directive@foo, that forms a cycle:@foo → InputObject → @foo.experimentalDirectivesOnDirectiveDefinitionsparser option is enabled, a directive applied directly to a directive definition or extension is also a reference. If@ais annotated with@band@bis annotated with@a, that is a cycle:@a → @b → @a.Cycles make it impossible to build a well-defined schema; the spec already forbids them, and experimental directive extensions do the same.
Key design question: should we consider existing schema applied directives?
In this initial attempt, when validating an SDL extension document against an existing schema, only references that appear in the document being validated are considered to close a cycle. Applied directives already persisted in the base schema are not counted as cycle-forming edges.
This means you can have a pre-existing schema like:
…where
@aalready carries@bin the stored schema, and then in your extension document write:This will not be reported as a cycle (
@a → @b → @a), because the@bedge on@acomes from the original schema, not from the document under validation. The rule only errors when at least one edge in the detected cycle is introduced by the new document. This is because applied directives, while accessible via the stored definitions/extensions, do not actually form part of the existing schema object => at least, per the specification.Is this the desired behavior? @BoD @graphql/tsc