Skip to content

Commit e94f8e3

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

13 files changed

+1841
-29
lines changed

src/execution/__tests__/stream-test.ts

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

src/execution/execute.ts

Lines changed: 310 additions & 24 deletions
Large diffs are not rendered by default.

src/validation/__tests__/DeferStreamDirectiveLabelRule-test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,63 @@ describe('Validate: Defer/Stream directive on root field', () => {
109109
},
110110
]);
111111
});
112+
it('Defer and stream with no label', () => {
113+
expectValid(`
114+
{
115+
dog {
116+
...dogFragment @defer
117+
}
118+
pets @stream(initialCount: 0) @stream {
119+
name
120+
}
121+
}
122+
fragment dogFragment on Dog {
123+
name
124+
}
125+
`);
126+
});
127+
it('Stream with variable label', () => {
128+
expectErrors(`
129+
query ($label: String!) {
130+
dog {
131+
...dogFragment @defer
132+
}
133+
pets @stream(initialCount: 0) @stream(label: $label) {
134+
name
135+
}
136+
}
137+
fragment dogFragment on Dog {
138+
name
139+
}
140+
`).toDeepEqual([
141+
{
142+
message:
143+
'Directive "stream"\'s label argument must be a static string.',
144+
locations: [{ line: 6, column: 39 }],
145+
},
146+
]);
147+
});
148+
it('Defer and stream with the same label', () => {
149+
expectErrors(`
150+
{
151+
dog {
152+
...dogFragment @defer(label: "MyLabel")
153+
}
154+
pets @stream(initialCount: 0) @stream(label: "MyLabel") {
155+
name
156+
}
157+
}
158+
fragment dogFragment on Dog {
159+
name
160+
}
161+
`).toDeepEqual([
162+
{
163+
message: 'Defer/Stream directive label argument must be unique.',
164+
locations: [
165+
{ line: 4, column: 26 },
166+
{ line: 6, column: 39 },
167+
],
168+
},
169+
]);
170+
});
112171
});

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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,114 @@ 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+
expectValid(`
202+
fragment conflictingArgs on Dog {
203+
name @stream
204+
name @stream
205+
}
206+
`);
207+
});
208+
101209
it('Same aliases with different field targets', () => {
102210
expectErrors(`
103211
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
@@ -63,6 +63,9 @@ export { ScalarLeafsRule } from './rules/ScalarLeafsRule';
6363
// Spec Section: "Subscriptions with Single Root Field"
6464
export { SingleFieldSubscriptionsRule } from './rules/SingleFieldSubscriptionsRule';
6565

66+
// Spec Section: "Stream Directives Are Used On List Fields"
67+
export { StreamDirectiveOnListFieldRule } from './rules/StreamDirectiveOnListFieldRule';
68+
6669
// Spec Section: "Argument Uniqueness"
6770
export { UniqueArgumentNamesRule } from './rules/UniqueArgumentNamesRule';
6871

0 commit comments

Comments
 (0)