Skip to content

Commit d883d55

Browse files
committed
implement @stream
1 parent 80f2a31 commit d883d55

File tree

8 files changed

+126
-23
lines changed

8 files changed

+126
-23
lines changed

lib/graphql/directive.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ def on_operation?
4848
require "graphql/directive/defer_directive"
4949
require "graphql/directive/include_directive"
5050
require "graphql/directive/skip_directive"
51+
require "graphql/directive/stream_directive"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
GraphQL::Directive::StreamDirective = GraphQL::Directive.define do
2+
name "stream"
3+
description "Push items from this list in sequential patches"
4+
locations([GraphQL::Directive::FIELD])
5+
6+
# This doesn't make any sense in this context
7+
# But it's required for DirectiveResolution
8+
include_proc -> (args) { true }
9+
end

lib/graphql/execution/deferred_execution.rb

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,41 @@ def execute(ast_operation, root_type, query_object)
101101
defers = initial_thread.defers
102102
while defers.any?
103103
next_defers = []
104-
defers.each do |deferred_frame|
104+
defers.each do |deferral|
105105
deferred_thread = ExecThread.new
106-
deferred_result = resolve_frame(scope, deferred_thread, deferred_frame)
107-
# No use patching for nil, that's there already
106+
case deferral
107+
when ExecFrame
108+
deferred_frame = deferral
109+
deferred_result = resolve_frame(scope, deferred_thread, deferred_frame)
110+
111+
when ExecStream
112+
begin
113+
list_frame = deferral.frame
114+
inner_type = deferral.type
115+
item, idx = deferral.enumerator.next
116+
deferred_frame = ExecFrame.new({
117+
node: list_frame.node,
118+
path: list_frame.path + [idx],
119+
type: inner_type,
120+
value: item,
121+
})
122+
deferred_result = resolve_value(scope, deferred_thread, deferred_frame, item, inner_type)
123+
deferred_thread.defers << deferral
124+
rescue StopIteration
125+
# The enum is done
126+
end
127+
else
128+
raise("Can't continue deferred #{deferred_frame.class.name}")
129+
end
130+
131+
# TODO: Should I patch nil?
108132
if !deferred_result.nil?
109133
collector.patch(
110134
path: ["data"] + deferred_frame.path,
111135
value: deferred_result
112136
)
113137
end
138+
114139
deferred_thread.errors.each do |deferred_error|
115140
collector.patch(
116141
path: ["errors", error_idx],
@@ -183,6 +208,20 @@ def initialize(node:, path:, type:, value:)
183208
end
184209
end
185210

211+
# Contains the list field's ExecFrame
212+
# And the enumerator which is being mapped
213+
# - {ExecStream#enumerator} is an Enumerator which yields `item, idx`
214+
# - {ExecStream#frame} is the {ExecFrame} for the list selection (where `@stream` was present)
215+
# - {ExecStream#type} is the inner type of the list (the item's type)
216+
class ExecStream
217+
attr_reader :enumerator, :frame, :type
218+
def initialize(enumerator:, frame:, type:)
219+
@enumerator = enumerator
220+
@frame = frame
221+
@type = type
222+
end
223+
end
224+
186225
private
187226

188227
# If this `frame` is marked as defer, add it to `defers`
@@ -327,16 +366,27 @@ def resolve_value(scope, thread, frame, value, type_defn)
327366
resolve_value(scope, thread, frame, value, wrapped_type)
328367
when GraphQL::TypeKinds::LIST
329368
wrapped_type = type_defn.of_type
330-
resolved_values = value.each_with_index.map do |item, idx|
331-
inner_frame = ExecFrame.new({
332-
node: frame.node,
333-
path: frame.path + [idx],
369+
items_enumerator = value.map.with_index
370+
if GraphQL::Execution::DirectiveChecks.stream?(frame.node)
371+
thread.defers << ExecStream.new(
372+
enumerator: items_enumerator,
373+
frame: frame,
334374
type: wrapped_type,
335-
value: item,
336-
})
337-
resolve_value(scope, thread, inner_frame, item, wrapped_type)
375+
)
376+
# The streamed list is empty in the initial resolve:
377+
[]
378+
else
379+
resolved_values = items_enumerator.each do |item, idx|
380+
inner_frame = ExecFrame.new({
381+
node: frame.node,
382+
path: frame.path + [idx],
383+
type: wrapped_type,
384+
value: item,
385+
})
386+
resolve_value(scope, thread, inner_frame, item, wrapped_type)
387+
end
388+
resolved_values
338389
end
339-
resolved_values
340390
when GraphQL::TypeKinds::INTERFACE, GraphQL::TypeKinds::UNION
341391
resolved_type = type_defn.resolve_type(value, scope)
342392

lib/graphql/execution/directive_checks.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module DirectiveChecks
66
SKIP = "skip"
77
INCLUDE = "include"
88
DEFER = "defer"
9+
STREAM = "stream"
910

1011
module_function
1112

@@ -14,6 +15,11 @@ def defer?(ast_node)
1415
ast_node.directives.any? { |dir| dir.name == DEFER }
1516
end
1617

18+
# @return [Boolean] Should this AST node be streamed?
19+
def stream?(ast_node)
20+
ast_node.directives.any? { |dir| dir.name == STREAM }
21+
end
22+
1723
# @return [Boolean] Should this AST node be skipped altogether?
1824
def skip?(ast_node, query)
1925
ast_node.directives.each do |ast_directive|

lib/graphql/schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Schema
1717
GraphQL::Directive::SkipDirective,
1818
GraphQL::Directive::IncludeDirective,
1919
GraphQL::Directive::DeferDirective,
20+
GraphQL::Directive::StreamDirective,
2021
]
2122
DYNAMIC_FIELDS = ["__type", "__typename", "__schema"]
2223

lib/graphql/static_validation/rules/directives_are_defined.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def validate(context)
1515
def validate_directive(ast_directive, directive_names, errors)
1616
if !directive_names.include?(ast_directive.name)
1717
errors << message("Directive @#{ast_directive.name} is not defined", ast_directive)
18+
GraphQL::Language::Visitor::SKIP
1819
end
1920
end
2021
end

spec/graphql/execution/deferred_execution_spec.rb

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,23 @@ def merged_result
4040
DummySchema.query_execution_strategy = @prev_execution_strategy
4141
end
4242

43-
let(:query_string) {%|
44-
{
45-
cheese(id: 1) {
46-
id
47-
flavor
48-
origin @defer
49-
cheeseSource: source @defer
50-
}
51-
}
52-
|}
53-
5443
let(:collector) { ArrayCollector.new }
5544
let(:result) {
5645
DummySchema.execute(query_string, context: {collector: collector})
5746
}
5847

5948
describe "@defer-ed fields" do
49+
let(:query_string) {%|
50+
{
51+
cheese(id: 1) {
52+
id
53+
flavor
54+
origin @defer
55+
cheeseSource: source @defer
56+
}
57+
}
58+
|}
59+
6060
it "emits them later" do
6161
result
6262
assert_equal 3, collector.patches.length
@@ -137,7 +137,7 @@ def merged_result
137137
}
138138
}
139139
|}
140-
it "patches the list, then the members" do
140+
it "patches the whole list, then the member fields" do
141141
result
142142
assert_equal 8, collector.patches.length
143143
expected_patches = [
@@ -239,4 +239,31 @@ def merged_result
239239
end
240240
end
241241
end
242+
243+
describe "@stream-ed fields" do
244+
let(:query_string) {%|
245+
{
246+
cheeses @stream {
247+
id
248+
}
249+
}
250+
|}
251+
252+
it "pushes an empty list, then each item" do
253+
result
254+
255+
assert_equal(4, collector.patches.length)
256+
assert_equal([], collector.patches[0][:path])
257+
assert_equal({"data" => { "cheeses" => [] } }, collector.patches[0][:value])
258+
assert_equal(["data", "cheeses", 0], collector.patches[1][:path])
259+
assert_equal({"id"=>1}, collector.patches[1][:value])
260+
assert_equal(["data", "cheeses", 1], collector.patches[2][:path])
261+
assert_equal({"id"=>2}, collector.patches[2][:value])
262+
assert_equal(["data", "cheeses", 2], collector.patches[3][:path])
263+
assert_equal({"id"=>3}, collector.patches[3][:value])
264+
end
265+
266+
it "supports lazy enumeration"
267+
it "defers nested defers"
268+
end
242269
end

spec/graphql/introspection/directive_type_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@
5050
"onFragment"=>true,
5151
"onOperation"=>false,
5252
},
53+
{
54+
"name"=>"stream",
55+
"args"=>[],
56+
"locations"=>["FIELD"],
57+
"onField"=>true,
58+
"onFragment"=>false,
59+
"onOperation"=>false,
60+
},
5361
]
5462
}
5563
}}

0 commit comments

Comments
 (0)