Skip to content

Commit 014a7d5

Browse files
committed
fix: DeferStreamDirectiveOnRootField forbids fragments on abstract types
Replicates graphql/graphql-js@3aad6a3
1 parent d850137 commit 014a7d5

2 files changed

Lines changed: 159 additions & 50 deletions

File tree

src/graphql/validation/rules/defer_stream_directive_on_root_field.py

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,121 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Any, cast
5+
from typing import TYPE_CHECKING, Any
66

77
from ...error import GraphQLError
8+
from ...language import (
9+
FieldNode,
10+
FragmentDefinitionNode,
11+
FragmentSpreadNode,
12+
InlineFragmentNode,
13+
OperationType,
14+
)
815
from ...type import GraphQLDeferDirective, GraphQLStreamDirective
9-
from . import ASTValidationRule, ValidationContext
16+
from . import ValidationRule
1017

1118
if TYPE_CHECKING:
12-
from ...language import DirectiveNode, Node
19+
from ...language import (
20+
DirectiveNode,
21+
OperationDefinitionNode,
22+
SelectionSetNode,
23+
)
24+
from ...type import GraphQLObjectType
1325

1426
__all__ = ["DeferStreamDirectiveOnRootField"]
1527

1628

17-
class DeferStreamDirectiveOnRootField(ASTValidationRule):
29+
def get_directive(
30+
node: FieldNode | FragmentSpreadNode | InlineFragmentNode, name: str
31+
) -> DirectiveNode | None:
32+
for directive in node.directives or ():
33+
if directive.name.value == name:
34+
return directive
35+
return None
36+
37+
38+
class DeferStreamDirectiveOnRootField(ValidationRule):
1839
"""Defer and stream directives are used on valid root field
1940
2041
A GraphQL document is only valid if defer directives are not used on root
2142
mutation or subscription types.
2243
"""
2344

24-
def enter_directive(
25-
self,
26-
node: DirectiveNode,
27-
_key: Any,
28-
_parent: Any,
29-
_path: Any,
30-
_ancestors: list[Node],
45+
def enter_operation_definition(
46+
self, node: OperationDefinitionNode, *_args: Any
3147
) -> None:
32-
context = cast("ValidationContext", self.context)
33-
parent_type = context.get_parent_type()
34-
if not parent_type:
48+
operation = node.operation
49+
if operation not in (OperationType.SUBSCRIPTION, OperationType.MUTATION):
3550
return
36-
schema = context.schema
37-
mutation_type = schema.mutation_type
38-
subscription_type = schema.subscription_type
51+
schema = self.context.schema
52+
root_type = schema.get_root_type(operation)
53+
if root_type:
54+
document = self.context.document
55+
fragments = {
56+
definition.name.value: definition
57+
for definition in document.definitions
58+
if isinstance(definition, FragmentDefinitionNode)
59+
}
60+
self.forbid_defer_stream(
61+
operation, root_type, fragments, node.selection_set, set()
62+
)
3963

40-
if node.name.value == GraphQLDeferDirective.name:
41-
if mutation_type and parent_type is mutation_type:
42-
self.report_error(
43-
GraphQLError(
44-
"Defer directive cannot be used on root"
45-
f" mutation type '{parent_type.name}'.",
46-
node,
47-
)
48-
)
49-
if subscription_type and parent_type is subscription_type:
50-
self.report_error(
51-
GraphQLError(
52-
"Defer directive cannot be used on root"
53-
f" subscription type '{parent_type.name}'.",
54-
node,
64+
def forbid_defer_stream(
65+
self,
66+
operation_type: OperationType,
67+
root_type: GraphQLObjectType,
68+
fragments: dict[str, FragmentDefinitionNode],
69+
selection_set: SelectionSetNode,
70+
visited_fragments: set[str],
71+
) -> None:
72+
for selection in selection_set.selections:
73+
if isinstance(selection, FieldNode):
74+
stream = get_directive(selection, GraphQLStreamDirective.name)
75+
if stream:
76+
self.report_error(
77+
GraphQLError(
78+
"Stream directive cannot be used on root"
79+
f" {operation_type.value} type '{root_type.name}'.",
80+
stream,
81+
)
5582
)
56-
)
57-
if node.name.value == GraphQLStreamDirective.name:
58-
if mutation_type and parent_type is mutation_type:
59-
self.report_error(
60-
GraphQLError(
61-
"Stream directive cannot be used on root"
62-
f" mutation type '{parent_type.name}'.",
63-
node,
83+
elif isinstance(selection, FragmentSpreadNode):
84+
fragment_name = selection.name.value
85+
if fragment_name in visited_fragments:
86+
continue
87+
fragment = fragments.get(fragment_name)
88+
if fragment:
89+
defer = get_directive(selection, GraphQLDeferDirective.name)
90+
if defer:
91+
self.report_error(
92+
GraphQLError(
93+
"Defer directive cannot be used on root"
94+
f" {operation_type.value} type '{root_type.name}'.",
95+
defer,
96+
)
97+
)
98+
self.forbid_defer_stream(
99+
operation_type,
100+
root_type,
101+
fragments,
102+
fragment.selection_set,
103+
visited_fragments,
64104
)
65-
)
66-
if subscription_type and parent_type is subscription_type:
67-
self.report_error(
68-
GraphQLError(
69-
"Stream directive cannot be used on root"
70-
f" subscription type '{parent_type.name}'.",
71-
node,
105+
visited_fragments.add(fragment_name)
106+
elif isinstance(selection, InlineFragmentNode):
107+
defer = get_directive(selection, GraphQLDeferDirective.name)
108+
if defer:
109+
self.report_error(
110+
GraphQLError(
111+
"Defer directive cannot be used on root"
112+
f" {operation_type.value} type '{root_type.name}'.",
113+
defer,
114+
)
72115
)
116+
self.forbid_defer_stream(
117+
operation_type,
118+
root_type,
119+
fragments,
120+
selection.selection_set,
121+
visited_fragments,
73122
)

tests/validation/test_defer_stream_directive_on_root_field.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,26 @@
1212
sender: String
1313
}
1414
15-
type SubscriptionRoot {
15+
interface Root {
16+
rootField: Message
17+
}
18+
19+
type SubscriptionRoot implements Root {
1620
subscriptionField: Message
1721
subscriptionListField: [Message]
22+
rootField: Message
1823
}
1924
20-
type MutationRoot {
25+
type MutationRoot implements Root {
2126
mutationField: Message
2227
mutationListField: [Message]
28+
rootField: Message
2329
}
2430
25-
type QueryRoot {
31+
type QueryRoot implements Root {
2632
message: Message
2733
messages: [Message]
34+
rootField: Message
2835
}
2936
3037
schema {
@@ -75,6 +82,13 @@ def defer_fragment_spread_on_root_mutation_field():
7582
"""
7683
mutation {
7784
...rootFragment @defer
85+
...otherFragment
86+
}
87+
fragment otherFragment on MutationRoot {
88+
...rootFragment
89+
mutationListField {
90+
body
91+
}
7892
}
7993
fragment rootFragment on MutationRoot {
8094
mutationField {
@@ -91,6 +105,29 @@ def defer_fragment_spread_on_root_mutation_field():
91105
],
92106
)
93107

108+
def defer_fragment_spread_on_root_mutation_field_interface():
109+
assert_errors(
110+
"""
111+
mutation {
112+
...rootFragment
113+
}
114+
fragment rootFragment on Root {
115+
... @defer {
116+
rootField {
117+
body
118+
}
119+
}
120+
}
121+
""",
122+
[
123+
{
124+
"message": "Defer directive cannot be used on root"
125+
" mutation type 'MutationRoot'.",
126+
"locations": [(6, 19)],
127+
},
128+
],
129+
)
130+
94131
def defer_inline_fragment_spread_on_root_mutation_field():
95132
assert_errors(
96133
"""
@@ -124,6 +161,29 @@ def defer_fragment_spread_on_nested_mutation_field():
124161
"""
125162
)
126163

164+
def defer_fragment_spread_on_root_subscription_field_interface():
165+
assert_errors(
166+
"""
167+
subscription {
168+
...rootFragment
169+
}
170+
fragment rootFragment on Root {
171+
... @defer {
172+
rootField {
173+
body
174+
}
175+
}
176+
}
177+
""",
178+
[
179+
{
180+
"message": "Defer directive cannot be used on root"
181+
" subscription type 'SubscriptionRoot'.",
182+
"locations": [(6, 19)],
183+
},
184+
],
185+
)
186+
127187
def defer_fragment_spread_on_root_subscription_field():
128188
assert_errors(
129189
"""

0 commit comments

Comments
 (0)