Skip to content

Commit 1774265

Browse files
committed
Implement support for @stream directive
# Conflicts: # src/execution/execute.ts # src/validation/index.d.ts # src/validation/index.ts
1 parent 709fc27 commit 1774265

11 files changed

+1675
-22
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 991 additions & 0 deletions
Large diffs are not rendered by default.

src/execution/execute.ts

Lines changed: 270 additions & 20 deletions
Large diffs are not rendered by default.

src/validation/__tests__/DeferStreamDirectiveOnRootFieldRule-test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const schema = buildSchema(`
3636
3737
type QueryRoot {
3838
message: Message
39+
messages: [Message]
3940
}
4041
4142
schema {
@@ -167,4 +168,91 @@ describe('Validate: Defer/Stream directive on root field', () => {
167168
}
168169
`);
169170
});
171+
it('Stream field on root query field', () => {
172+
expectValid(`
173+
{
174+
messages @stream {
175+
name
176+
}
177+
}
178+
`);
179+
});
180+
it('Stream field on fragment on root query field', () => {
181+
expectValid(`
182+
{
183+
...rootFragment
184+
}
185+
fragment rootFragment on QueryType {
186+
messages @stream {
187+
name
188+
}
189+
}
190+
`);
191+
});
192+
it('Stream field on root mutation field', () => {
193+
expectErrors(`
194+
mutation {
195+
mutationListField @stream {
196+
name
197+
}
198+
}
199+
`).toDeepEqual([
200+
{
201+
message:
202+
'Stream directive cannot be used on root mutation type "MutationRoot".',
203+
locations: [{ line: 3, column: 27 }],
204+
},
205+
]);
206+
});
207+
it('Stream field on fragment on root mutation field', () => {
208+
expectErrors(`
209+
mutation {
210+
...rootFragment
211+
}
212+
fragment rootFragment on MutationRoot {
213+
mutationListField @stream {
214+
name
215+
}
216+
}
217+
`).toDeepEqual([
218+
{
219+
message:
220+
'Stream directive cannot be used on root mutation type "MutationRoot".',
221+
locations: [{ line: 6, column: 27 }],
222+
},
223+
]);
224+
});
225+
it('Stream field on root subscription field', () => {
226+
expectErrors(`
227+
subscription {
228+
subscriptionListField @stream {
229+
name
230+
}
231+
}
232+
`).toDeepEqual([
233+
{
234+
message:
235+
'Stream directive cannot be used on root subscription type "SubscriptionRoot".',
236+
locations: [{ line: 3, column: 31 }],
237+
},
238+
]);
239+
});
240+
it('Stream field on fragment on root subscription field', () => {
241+
expectErrors(`
242+
subscription {
243+
...rootFragment
244+
}
245+
fragment rootFragment on SubscriptionRoot {
246+
subscriptionListField @stream {
247+
name
248+
}
249+
}
250+
`).toDeepEqual([
251+
{
252+
message:
253+
'Stream directive cannot be used on root subscription type "SubscriptionRoot".',
254+
locations: [{ line: 6, column: 31 }],
255+
},
256+
]);
257+
});
170258
});

src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,123 @@ describe('Validate: Overlapping fields can be merged', () => {
9898
`);
9999
});
100100

101+
it('Same stream directives supported', () => {
102+
expectValid(`
103+
fragment differentDirectivesWithDifferentAliases on Dog {
104+
name @stream(label: "streamLabel", initialCount: 1)
105+
name @stream(label: "streamLabel", initialCount: 1)
106+
}
107+
`);
108+
});
109+
110+
it('different stream directive label', () => {
111+
expectErrors(`
112+
fragment conflictingArgs on Dog {
113+
name @stream(label: "streamLabel", initialCount: 1)
114+
name @stream(label: "anotherLabel", initialCount: 1)
115+
}
116+
`).toDeepEqual([
117+
{
118+
message:
119+
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
120+
locations: [
121+
{ line: 3, column: 9 },
122+
{ line: 4, column: 9 },
123+
],
124+
},
125+
]);
126+
});
127+
128+
it('different stream directive initialCount', () => {
129+
expectErrors(`
130+
fragment conflictingArgs on Dog {
131+
name @stream(label: "streamLabel", initialCount: 1)
132+
name @stream(label: "streamLabel", initialCount: 2)
133+
}
134+
`).toDeepEqual([
135+
{
136+
message:
137+
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
138+
locations: [
139+
{ line: 3, column: 9 },
140+
{ line: 4, column: 9 },
141+
],
142+
},
143+
]);
144+
});
145+
146+
it('different stream directive first missing args', () => {
147+
expectErrors(`
148+
fragment conflictingArgs on Dog {
149+
name @stream
150+
name @stream(label: "streamLabel", initialCount: 1)
151+
}
152+
`).toDeepEqual([
153+
{
154+
message:
155+
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
156+
locations: [
157+
{ line: 3, column: 9 },
158+
{ line: 4, column: 9 },
159+
],
160+
},
161+
]);
162+
});
163+
164+
it('different stream directive second missing args', () => {
165+
expectErrors(`
166+
fragment conflictingArgs on Dog {
167+
name @stream(label: "streamLabel", initialCount: 1)
168+
name @stream
169+
}
170+
`).toDeepEqual([
171+
{
172+
message:
173+
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
174+
locations: [
175+
{ line: 3, column: 9 },
176+
{ line: 4, column: 9 },
177+
],
178+
},
179+
]);
180+
});
181+
182+
it('mix of stream and no stream', () => {
183+
expectErrors(`
184+
fragment conflictingArgs on Dog {
185+
name @stream
186+
name
187+
}
188+
`).toDeepEqual([
189+
{
190+
message:
191+
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
192+
locations: [
193+
{ line: 3, column: 9 },
194+
{ line: 4, column: 9 },
195+
],
196+
},
197+
]);
198+
});
199+
200+
it('different stream directive both missing args', () => {
201+
expectErrors(`
202+
fragment conflictingArgs on Dog {
203+
name @stream
204+
name @stream
205+
}
206+
`).toDeepEqual([
207+
{
208+
message:
209+
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
210+
locations: [
211+
{ line: 3, column: 9 },
212+
{ line: 4, column: 9 },
213+
],
214+
},
215+
]);
216+
});
217+
101218
it('Same aliases with different field targets', () => {
102219
expectErrors(`
103220
fragment sameAliasesWithDifferentFieldTargets on Dog {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it } from 'mocha';
2+
3+
import { StreamDirectiveOnListFieldRule } from '../rules/StreamDirectiveOnListFieldRule';
4+
5+
import { expectValidationErrors } from './harness';
6+
7+
function expectErrors(queryStr: string) {
8+
return expectValidationErrors(StreamDirectiveOnListFieldRule, queryStr);
9+
}
10+
11+
function expectValid(queryStr: string) {
12+
expectErrors(queryStr).toDeepEqual([]);
13+
}
14+
15+
describe('Validate: Stream directive on list field', () => {
16+
it('Stream on list field', () => {
17+
expectValid(`
18+
fragment objectFieldSelection on Human {
19+
pets @stream(initialCount: 0) {
20+
name
21+
}
22+
}
23+
`);
24+
});
25+
26+
it('Stream on non-null list field', () => {
27+
expectValid(`
28+
fragment objectFieldSelection on Human {
29+
relatives @stream(initialCount: 0) {
30+
name
31+
}
32+
}
33+
`);
34+
});
35+
36+
it("Doesn't validate other directives on list fields", () => {
37+
expectValid(`
38+
fragment objectFieldSelection on Human {
39+
pets @include(if: true) {
40+
name
41+
}
42+
}
43+
`);
44+
});
45+
46+
it("Doesn't validate other directives on non-list fields", () => {
47+
expectValid(`
48+
fragment objectFieldSelection on Human {
49+
pets {
50+
name @include(if: true)
51+
}
52+
}
53+
`);
54+
});
55+
56+
it("Doesn't validate misplaced stream directives", () => {
57+
expectValid(`
58+
fragment objectFieldSelection on Human {
59+
... @stream(initialCount: 0) {
60+
name
61+
}
62+
}
63+
`);
64+
});
65+
66+
it('reports errors when stream is used on non-list field', () => {
67+
expectErrors(`
68+
fragment objectFieldSelection on Human {
69+
name @stream(initialCount: 0)
70+
}
71+
`).toDeepEqual([
72+
{
73+
message:
74+
'Stream directive cannot be used on non-list field "name" on type "Human".',
75+
locations: [{ line: 3, column: 14 }],
76+
},
77+
]);
78+
});
79+
});

src/validation/__tests__/harness.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const testSchema: GraphQLSchema = buildSchema(`
5858
type Human {
5959
name(surname: Boolean): String
6060
pets: [Pet]
61-
relatives: [Human]
61+
relatives: [Human]!
6262
}
6363
6464
enum FurColor {

src/validation/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export { ScalarLeafsRule } from './rules/ScalarLeafsRule';
6060
/** Spec Section: "Subscriptions with Single Root Field" */
6161
export { SingleFieldSubscriptionsRule } from './rules/SingleFieldSubscriptionsRule';
6262

63+
/** Spec Section: "Stream Directives Are Used On List Fields" */
64+
export { StreamDirectiveOnListFieldRule } from './rules/StreamDirectiveOnListFieldRule';
65+
6366
/** Spec Section: "Argument Uniqueness" */
6467
export { UniqueArgumentNamesRule } from './rules/UniqueArgumentNamesRule';
6568

src/validation/rules/DeferStreamDirectiveOnRootFieldRule.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { GraphQLError } from '../../error/GraphQLError';
22

33
import type { ASTVisitor } from '../../language/visitor';
44

5-
import { GraphQLDeferDirective } from '../../type/directives';
5+
import {
6+
GraphQLDeferDirective,
7+
GraphQLStreamDirective,
8+
} from '../../type/directives';
69

710
import type { ValidationContext } from '../ValidationContext';
811

@@ -37,6 +40,24 @@ export function DeferStreamDirectiveOnRootFieldRule(
3740
);
3841
}
3942
}
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.name}".`,
48+
node,
49+
),
50+
);
51+
}
52+
if (subscriptionType && parentType === subscriptionType) {
53+
context.reportError(
54+
new GraphQLError(
55+
`Stream directive cannot be used on root subscription type "${parentType.name}".`,
56+
node,
57+
),
58+
);
59+
}
60+
}
4061
},
4162
};
4263
}

0 commit comments

Comments
 (0)