Skip to content

Commit 78cf0f7

Browse files
authored
Merge pull request #5547 from rmosolgo/execution-next-auth
Exec-next: support authorization hooks
2 parents a2cbf25 + f49b016 commit 78cf0f7

21 files changed

Lines changed: 205 additions & 138 deletions

Gemfile

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ gem 'stackprof', platform: :ruby
88
gem 'pry'
99
gem 'pry-stack_explorer', platform: :ruby
1010

11-
if RUBY_VERSION >= "3.0"
12-
gem "libev_scheduler"
13-
gem "evt"
14-
end
15-
1611
if RUBY_VERSION >= "3.2.0"
17-
gem "minitest-mock"
1812
gem "async", "~>2.0"
1913
gem "minitest-mock"
2014
end

gemfiles/mongoid_8.gemfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ gem "ruby-prof", platform: :ruby
88
gem "pry"
99
gem "pry-stack_explorer", platform: :ruby
1010
gem "mongoid", "~> 8.0"
11-
gem "libev_scheduler"
12-
gem "evt"
1311
gem "async"
1412
gem "concurrent-ruby", "1.3.4"
1513
gem "minitest-mock"

gemfiles/mongoid_9.gemfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ gem "ruby-prof", platform: :ruby
77
gem "pry"
88
gem "pry-stack_explorer", platform: :ruby
99
gem "mongoid", "~> 9.0"
10-
gem "libev_scheduler"
11-
gem "evt"
1210
gem "async"
1311
gem "minitest-mock"
1412

gemfiles/rails_7.2_postgresql.gemfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ gem "pry-stack_explorer", platform: :ruby
99
gem "rails", "~> 7.2.0", require: "rails/all"
1010
gem "pg", platform: :ruby
1111
gem "sequel"
12-
gem "evt"
1312
gem "async"
14-
gem "libev_scheduler"
1513
gem "google-protobuf"
1614

1715
gemspec path: "../"

gemfiles/rails_8.0.gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ gem "rails", "~> 8.0.0", require: "rails/all"
1010
gem "sqlite3"
1111
gem "pg", platform: :ruby
1212
gem "sequel"
13-
gem "evt"
1413
gem "async"
1514
gem "google-protobuf"
1615
gem "minitest-mock"

gemfiles/rails_8.1.gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ gem "rails", "~> 8.1.0", require: "rails/all"
1010
gem "sqlite3"
1111
gem "pg", platform: :ruby
1212
gem "sequel"
13-
gem "evt"
1413
gem "async"
1514
gem "google-protobuf"
1615
gem "minitest-mock"

gemfiles/rails_master.gemfile

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ gem "rails", github: "rails/rails", require: "rails/all", ref: "main"
1010
gem 'sqlite3'
1111
gem 'pg'
1212
gem "sequel"
13-
gem "evt"
14-
if RUBY_ENGINE == "ruby" # This doesn't work on truffle-ruby because there's no `IO::READABLE`
15-
gem "libev_scheduler"
16-
end
1713
gem "async"
1814
gem "google-protobuf"
1915
gem "redis"

lib/graphql.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ def self.eager_load!
2121
class Error < StandardError
2222
end
2323

24+
class RuntimeError < Error
25+
end
26+
2427
# This error is raised when GraphQL-Ruby encounters a situation
2528
# that it *thought* would never happen. Please report this bug!
2629
class InvariantError < Error

lib/graphql/execution/batching/field_compatibility.rb

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,22 @@ def resolve_batch(frs, objects, context, kwargs)
6868
end
6969

7070
if @owner.method_defined?(@resolver_method)
71-
frs.selections_step.graphql_objects.map do |obj_inst|
72-
if dynamic_introspection
73-
obj_inst = @owner.wrap(obj_inst, context)
74-
end
75-
with_extensions(obj_inst, kwargs, context) do |obj, ruby_kwargs|
76-
if ruby_kwargs.empty?
77-
obj.public_send(@resolver_method)
78-
else
79-
obj.public_send(@resolver_method, **ruby_kwargs)
71+
results = []
72+
frs.selections_step.graphql_objects.each_with_index do |obj_inst, idx|
73+
if frs.object_is_authorized[idx]
74+
if dynamic_introspection
75+
obj_inst = @owner.wrap(obj_inst, context)
76+
end
77+
results << with_extensions(obj_inst, kwargs, context) do |obj, ruby_kwargs|
78+
if ruby_kwargs.empty?
79+
obj.public_send(@resolver_method)
80+
else
81+
obj.public_send(@resolver_method, **ruby_kwargs)
82+
end
8083
end
8184
end
8285
end
86+
results
8387
elsif @resolver_class
8488
objects.map do |o|
8589
resolver_inst_kwargs = kwargs.dup
@@ -106,17 +110,13 @@ def resolve_batch(frs, objects, context, kwargs)
106110
new_return_value
107111
end
108112
end
109-
rescue GraphQL::Error => err
110-
err.path = frs.path
111-
context.errors << err
112-
nil
113+
rescue RuntimeError => err
114+
err
113115
rescue StandardError => stderr
114116
begin
115117
context.query.handle_or_reraise(stderr)
116118
rescue GraphQL::ExecutionError => ex_err
117-
ex_err.path = frs.path
118-
context.errors << ex_err
119-
nil
119+
ex_err
120120
end
121121
end
122122
elsif objects.first.is_a?(Hash)
@@ -128,15 +128,19 @@ def resolve_batch(frs, objects, context, kwargs)
128128
if extensions.empty?
129129
objects.map { |o| o.public_send(@method_sym)}
130130
else
131-
frs.selections_step.graphql_objects.map do |obj_inst|
132-
with_extensions(obj_inst, EmptyObjects::EMPTY_HASH, context) do |obj, arguments|
133-
if arguments.empty?
134-
obj.object.public_send(@method_sym)
135-
else
136-
obj.object.public_send(@method_sym, **arguments)
131+
results = []
132+
frs.selections_step.graphql_objects.each_with_index do |obj_inst, idx|
133+
if frs.object_is_authorized[idx]
134+
results << with_extensions(obj_inst, EmptyObjects::EMPTY_HASH, context) do |obj, arguments|
135+
if arguments.empty?
136+
obj.object.public_send(@method_sym)
137+
else
138+
obj.object.public_send(@method_sym, **arguments)
139+
end
137140
end
138141
end
139142
end
143+
results
140144
end
141145
end
142146
end

lib/graphql/execution/batching/field_resolve_step.rb

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def initialize(parent_type:, runner:, key:, selections_step:)
1010
@ast_node = @ast_nodes = nil
1111
@runner = runner
1212
@field_definition = nil
13+
@arguments = nil
1314
@field_results = nil
1415
@path = nil
1516
@enqueued_authorization = false
@@ -18,9 +19,10 @@ def initialize(parent_type:, runner:, key:, selections_step:)
1819
@all_next_results = nil
1920
@static_type = nil
2021
@next_selections = nil
22+
@object_is_authorized = nil
2123
end
2224

23-
attr_reader :ast_node, :key, :parent_type, :selections_step, :runner, :field_definition
25+
attr_reader :ast_node, :key, :parent_type, :selections_step, :runner, :field_definition, :object_is_authorized, :arguments
2426

2527
def path
2628
@path ||= [*@selections_step.path, @key].freeze
@@ -103,7 +105,16 @@ def coerce_argument_value(arg_t, arg_value)
103105
arg_value.map { |v| coerce_argument_value(inner_t, v) }
104106
end
105107
elsif arg_t.kind.leaf?
106-
arg_t.coerce_input(arg_value, @selections_step.query.context)
108+
begin
109+
ctx = @selections_step.query.context
110+
arg_t.coerce_input(arg_value, ctx)
111+
rescue GraphQL::UnauthorizedEnumValueError => enum_err
112+
begin
113+
@runner.schema.unauthorized_object(enum_err)
114+
rescue GraphQL::ExecutionError => ex_err
115+
ex_err
116+
end
117+
end
107118
elsif arg_t.kind.input_object?
108119
coerce_arguments(arg_t, arg_value)
109120
else
@@ -124,11 +135,9 @@ def sync(lazy)
124135
else
125136
@runner.schema.sync_lazy(lazy)
126137
end
127-
rescue GraphQL::UnauthorizedError => err
128-
@runner.schema.unauthorized_object(err)
138+
rescue GraphQL::UnauthorizedError => auth_err
139+
@runner.schema.unauthorized_object(auth_err)
129140
rescue GraphQL::ExecutionError => err
130-
err.path = path
131-
err.ast_nodes = ast_nodes
132141
err
133142
end
134143

@@ -142,19 +151,51 @@ def call
142151
end
143152
end
144153

154+
def add_graphql_error(err)
155+
err.path = path
156+
err.ast_nodes = ast_nodes
157+
@selections_step.query.context.add_error(err)
158+
err
159+
end
160+
161+
module AlwaysAuthorized
162+
def self.[](_key)
163+
true
164+
end
165+
end
166+
145167
def execute_field
146168
field_name = @ast_node.name
147169
@field_definition = @selections_step.query.get_field(@parent_type, field_name) || raise("Invariant: no field found for #{@parent_type.to_type_signature}.#{ast_node.name}")
148170
objects = @selections_step.objects
149171
if field_name == "__typename"
172+
# TODO handle custom introspection
150173
@field_results = Array.new(objects.size, @parent_type.graphql_name)
174+
@object_is_authorized = AlwaysAuthorized
151175
build_results
152176
return
153177
end
154178

155-
arguments = coerce_arguments(@field_definition, @ast_node.arguments) # rubocop:disable Development/ContextIsPassedCop
179+
@arguments = coerce_arguments(@field_definition, @ast_node.arguments) # rubocop:disable Development/ContextIsPassedCop
156180

157-
@field_results = @field_definition.resolve_batch(self, objects, @selections_step.query.context, arguments)
181+
182+
ctx = @selections_step.query.context
183+
184+
if (@runner.authorizes.fetch(@field_definition) { @runner.authorizes[@field_definition] = @field_definition.authorizes?(ctx) })
185+
authorized_objects = []
186+
@object_is_authorized = objects.map { |o|
187+
is_authed = @field_definition.authorized?(o, @arguments, ctx)
188+
if is_authed
189+
authorized_objects << o
190+
end
191+
is_authed
192+
}
193+
else
194+
authorized_objects = objects
195+
@object_is_authorized = AlwaysAuthorized
196+
end
197+
198+
@field_results = @field_definition.resolve_batch(self, authorized_objects, ctx, @arguments)
158199

159200
if @runner.resolves_lazies # TODO extract this
160201
lazies = false
@@ -205,8 +246,14 @@ def build_results
205246
is_list = return_type.list?
206247
is_non_null = return_type.non_null?
207248
results = @selections_step.results
208-
@field_results.each_with_index do |result, i|
209-
result_h = results[i]
249+
field_result_idx = 0
250+
results.each_with_index do |result_h, i|
251+
if @object_is_authorized[i]
252+
result = @field_results[field_result_idx]
253+
field_result_idx += 1
254+
else
255+
result = nil
256+
end
210257
build_graphql_result(result_h, @key, result, return_type, is_non_null, is_list, false)
211258
end
212259
@enqueued_authorization = true
@@ -219,22 +266,25 @@ def build_results
219266
else
220267
results = @selections_step.results
221268
ctx = @selections_step.query.context
222-
@field_results.each_with_index do |result, i|
223-
result_h = results[i]
224-
result_h[@key] = if result.nil?
269+
field_result_idx = 0
270+
results.each_with_index do |result_h, i|
271+
if @object_is_authorized[i]
272+
field_result = @field_results[field_result_idx]
273+
field_result_idx += 1
274+
else
275+
field_result = nil
276+
end
277+
result_h[@key] = if field_result.nil?
225278
if return_type.non_null?
226279
add_non_null_error(false)
227280
else
228281
nil
229282
end
230-
elsif result.is_a?(GraphQL::Error)
231-
result.path = path
232-
result.ast_nodes = ast_nodes
233-
ctx.add_error(result)
234-
result
283+
elsif field_result.is_a?(GraphQL::Error)
284+
add_graphql_error(field_result)
235285
else
236286
# TODO `nil`s in [T!] types aren't handled
237-
return_type.coerce_result(result, ctx)
287+
return_type.coerce_result(field_result, ctx)
238288
end
239289
end
240290
end
@@ -308,10 +358,7 @@ def build_graphql_result(graphql_result, key, field_result, return_type, is_nn,
308358
graphql_result[key] = nil
309359
end
310360
elsif field_result.is_a?(GraphQL::Error)
311-
field_result.path = path
312-
field_result.ast_nodes = ast_nodes
313-
@selections_step.query.context.add_error(field_result)
314-
graphql_result[key] = field_result
361+
graphql_result[key] = add_graphql_error(field_result)
315362
elsif is_list
316363
if is_nn
317364
return_type = return_type.of_type

0 commit comments

Comments
 (0)