Skip to content

Commit 2ba3eb5

Browse files
committed
Rewrite DeferStreamDirectiveOnRootFieldRule to catch usage in fragments with abstract types
1 parent 60272fc commit 2ba3eb5

2 files changed

Lines changed: 167 additions & 38 deletions

File tree

src/validation/__tests__/DeferStreamDirectiveOnRootFieldRule-test.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,26 @@ const schema = buildSchema(`
2424
sender: String
2525
}
2626
27-
type SubscriptionRoot {
27+
interface Root {
28+
rootField: Message
29+
}
30+
31+
type SubscriptionRoot implements Root {
2832
subscriptionField: Message
2933
subscriptionListField: [Message]
34+
rootField: Message
3035
}
3136
32-
type MutationRoot {
37+
type MutationRoot implements Root {
3338
mutationField: Message
3439
mutationListField: [Message]
40+
rootField: Message
3541
}
3642
37-
type QueryRoot {
43+
type QueryRoot implements Root {
3844
message: Message
3945
messages: [Message]
46+
rootField: Message
4047
}
4148
4249
schema {
@@ -76,6 +83,13 @@ describe('Validate: Defer/Stream directive on root field', () => {
7683
expectErrors(`
7784
mutation {
7885
...rootFragment @defer
86+
...otherFragment
87+
}
88+
fragment otherFragment on MutationRoot {
89+
...rootFragment
90+
mutationListField {
91+
body
92+
}
7993
}
8094
fragment rootFragment on MutationRoot {
8195
mutationField {
@@ -107,7 +121,26 @@ describe('Validate: Defer/Stream directive on root field', () => {
107121
},
108122
]);
109123
});
110-
124+
it('Defer fragment spread on root mutation field interface', () => {
125+
expectErrors(`
126+
mutation {
127+
...rootFragment
128+
}
129+
fragment rootFragment on Root {
130+
... @defer {
131+
rootField {
132+
body
133+
}
134+
}
135+
}
136+
`).toDeepEqual([
137+
{
138+
message:
139+
'Defer directive cannot be used on root mutation type "MutationRoot".',
140+
locations: [{ line: 6, column: 13 }],
141+
},
142+
]);
143+
});
111144
it('Defer fragment spread on nested mutation field', () => {
112145
expectValid(`
113146
mutation {
@@ -120,6 +153,26 @@ describe('Validate: Defer/Stream directive on root field', () => {
120153
`);
121154
});
122155

156+
it('Defer fragment spread on root subscription field interface', () => {
157+
expectErrors(`
158+
subscription {
159+
...rootFragment
160+
}
161+
fragment rootFragment on Root {
162+
... @defer {
163+
rootField {
164+
body
165+
}
166+
}
167+
}
168+
`).toDeepEqual([
169+
{
170+
message:
171+
'Defer directive cannot be used on root subscription type "SubscriptionRoot".',
172+
locations: [{ line: 6, column: 13 }],
173+
},
174+
]);
175+
});
123176
it('Defer fragment spread on root subscription field', () => {
124177
expectErrors(`
125178
subscription {
Lines changed: 110 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { GraphQLError } from '../../error/GraphQLError.js';
22

3+
import type {
4+
FragmentDefinitionNode,
5+
FragmentSpreadNode,
6+
InlineFragmentNode,
7+
OperationDefinitionNode,
8+
OperationTypeNode,
9+
SelectionSetNode,
10+
} from '../../language/ast.js';
11+
import { Kind } from '../../language/kinds.js';
312
import type { ASTVisitor } from '../../language/visitor.js';
413

514
import {
615
GraphQLDeferDirective,
716
GraphQLStreamDirective,
817
} from '../../type/directives.js';
18+
import type { GraphQLObjectType } from '../../type/index.js';
919

1020
import type { ValidationContext } from '../ValidationContext.js';
1121

@@ -18,46 +28,112 @@ export function DeferStreamDirectiveOnRootFieldRule(
1828
context: ValidationContext,
1929
): ASTVisitor {
2030
return {
21-
Directive(node) {
22-
const mutationType = context.getSchema().getMutationType();
23-
const subscriptionType = context.getSchema().getSubscriptionType();
24-
const parentType = context.getParentType();
25-
if (parentType && node.name.value === GraphQLDeferDirective.name) {
26-
if (mutationType && parentType === mutationType) {
27-
context.reportError(
28-
new GraphQLError(
29-
`Defer directive cannot be used on root mutation type "${parentType}".`,
30-
{ nodes: node },
31-
),
32-
);
33-
}
34-
if (subscriptionType && parentType === subscriptionType) {
35-
context.reportError(
36-
new GraphQLError(
37-
`Defer directive cannot be used on root subscription type "${parentType}".`,
38-
{ nodes: node },
39-
),
40-
);
31+
OperationDefinition(node: OperationDefinitionNode) {
32+
const document = context.getDocument();
33+
const fragments = new Map<string, FragmentDefinitionNode>();
34+
35+
for (const definition of document.definitions) {
36+
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
37+
fragments.set(definition.name.value, definition);
4138
}
4239
}
43-
if (parentType && node.name.value === GraphQLStreamDirective.name) {
44-
if (mutationType && parentType === mutationType) {
45-
context.reportError(
46-
new GraphQLError(
47-
`Stream directive cannot be used on root mutation type "${parentType}".`,
48-
{ nodes: node },
49-
),
50-
);
51-
}
52-
if (subscriptionType && parentType === subscriptionType) {
40+
if (node.operation !== 'subscription' && node.operation !== 'mutation') {
41+
return;
42+
}
43+
const schema = context.getSchema();
44+
const rootType = schema.getRootType(node.operation);
45+
if (rootType) {
46+
collectRootFields({
47+
context,
48+
operationType: node.operation,
49+
rootType,
50+
fragments,
51+
selectionSet: node.selectionSet,
52+
visitedFragments: new Set(),
53+
});
54+
}
55+
},
56+
};
57+
}
58+
59+
function collectRootFields({
60+
context,
61+
operationType,
62+
rootType,
63+
fragments,
64+
selectionSet,
65+
visitedFragments,
66+
}: {
67+
context: ValidationContext;
68+
operationType: OperationTypeNode;
69+
rootType: GraphQLObjectType;
70+
fragments: Map<string, FragmentDefinitionNode>;
71+
selectionSet: SelectionSetNode;
72+
visitedFragments: Set<string>;
73+
}) {
74+
for (const selection of selectionSet.selections) {
75+
if (selection.kind === 'Field') {
76+
const stream = selection.directives?.find(
77+
(d) => d.name.value === GraphQLStreamDirective.name,
78+
);
79+
if (stream) {
80+
context.reportError(
81+
new GraphQLError(
82+
`Stream directive cannot be used on root ${operationType} type "${rootType}".`,
83+
{ nodes: stream },
84+
),
85+
);
86+
}
87+
} else if (selection.kind === 'FragmentSpread') {
88+
const fragmentName = selection.name.value;
89+
if (visitedFragments.has(fragmentName)) {
90+
continue;
91+
}
92+
const fragment = fragments.get(fragmentName);
93+
if (fragment) {
94+
const defer = getDeferDirective(selection);
95+
if (defer) {
5396
context.reportError(
5497
new GraphQLError(
55-
`Stream directive cannot be used on root subscription type "${parentType}".`,
56-
{ nodes: node },
98+
`Defer directive cannot be used on root ${operationType} type "${rootType}".`,
99+
{ nodes: defer },
57100
),
58101
);
59102
}
103+
collectRootFields({
104+
context,
105+
operationType,
106+
rootType,
107+
fragments,
108+
selectionSet: fragment.selectionSet,
109+
visitedFragments,
110+
});
60111
}
61-
},
62-
};
112+
visitedFragments.add(fragmentName);
113+
} else if (selection.kind === 'InlineFragment') {
114+
const defer = getDeferDirective(selection);
115+
if (defer) {
116+
context.reportError(
117+
new GraphQLError(
118+
`Defer directive cannot be used on root ${operationType} type "${rootType}".`,
119+
{ nodes: defer },
120+
),
121+
);
122+
}
123+
collectRootFields({
124+
context,
125+
operationType,
126+
rootType,
127+
fragments,
128+
selectionSet: selection.selectionSet,
129+
visitedFragments,
130+
});
131+
}
132+
}
133+
}
134+
135+
function getDeferDirective(fragment: FragmentSpreadNode | InlineFragmentNode) {
136+
return fragment.directives?.find(
137+
(d) => d.name.value === GraphQLDeferDirective.name,
138+
);
63139
}

0 commit comments

Comments
 (0)