Skip to content

Commit 9ad9074

Browse files
committed
wip
1 parent f88ec1f commit 9ad9074

File tree

11 files changed

+131
-19
lines changed

11 files changed

+131
-19
lines changed

lib/graphql/execution.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,12 @@ class Skip < GraphQL::Error; end
1414
# Just a singleton for implementing {Query::Context#skip}
1515
# @api private
1616
SKIP = Skip.new
17+
18+
# @api private
19+
class SkipFromParentList < GraphQL::Error; end
20+
21+
# Just a singleton for implementing {Query::Context#skip}
22+
# @api private
23+
SKIP_FROM_PARENT_LIST = SkipFromParentList.new
1724
end
1825
end

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ def initialize(result_name, parent_result)
3131
# @return [nil, true]
3232
attr_accessor :graphql_non_null_list_items
3333

34+
# @return [nil, true]
35+
attr_accessor :graphql_skip_list_items_that_raise
36+
37+
def has_graphql_graph_parent_that_skips_list_items_that_raise
38+
return @has_graphql_graph_parent_that_skips_list_items_that_raise if defined?(@has_graphql_graph_parent_that_skips_list_items_that_raise)
39+
40+
@has_graphql_graph_parent_that_skips_list_items_that_raise = graphql_skip_list_items_that_raise ||
41+
!!graphql_parent&.has_graphql_graph_parent_that_skips_list_items_that_raise
42+
end
43+
3444
# @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
3545
attr_accessor :graphql_result_data
3646
end
@@ -513,13 +523,18 @@ def evaluate_selection_with_args(arguments, field_defn, next_path, ast_node, fie
513523
rescue GraphQL::ExecutionError => err
514524
err
515525
rescue StandardError => err
516-
begin
517-
query.handle_or_reraise(err)
518-
rescue GraphQL::ExecutionError => ex_err
519-
ex_err
526+
if selection_result.has_graphql_graph_parent_that_skips_list_items_that_raise
527+
context.skip_from_parent_list
528+
else
529+
begin
530+
query.handle_or_reraise(err)
531+
rescue GraphQL::ExecutionError => ex_err
532+
ex_err
533+
end
520534
end
521535
end
522536
after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result) do |inner_result|
537+
# puts("return type: #{return_type.to_type_signature}, skip_nodes_on_raise: #{return_type.skip_nodes_on_raise?}")
523538
continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
524539
if HALT != continue_value
525540
continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result)
@@ -545,7 +560,27 @@ def dead_result?(selection_result)
545560

546561
def set_result(selection_result, result_name, value)
547562
if !dead_result?(selection_result)
548-
if value.nil? &&
563+
if value == GraphQL::Execution::SKIP_FROM_PARENT_LIST
564+
if selection_result.graphql_skip_list_items_that_raise
565+
case selection_result
566+
when GraphQLResultHash then selection_result.delete(result_name) # TODO: unify `#delete` interface with `#graphql_skip_at`
567+
when GraphQLResultArray then selection_result.graphql_skip_at(result_name)
568+
else raise "huh?"
569+
end
570+
else
571+
# Propograte up to find the first list this item can be skiped from.
572+
#
573+
parent = selection_result.graphql_parent
574+
name_in_parent = selection_result.graphql_result_name
575+
if parent.nil? # This is a top-level result hash
576+
@response = nil
577+
else
578+
set_result(parent, name_in_parent, GraphQL::Execution::SKIP_FROM_PARENT_LIST)
579+
set_graphql_dead(selection_result)
580+
end
581+
end
582+
583+
elsif value.nil? &&
549584
( # there are two conditions under which `nil` is not allowed in the response:
550585
(selection_result.graphql_non_null_list_items) || # this value would be written into a list that doesn't allow nils
551586
((nn = selection_result.graphql_non_null_field_names) && nn.include?(result_name)) # this value would be written into a field that doesn't allow nils
@@ -647,6 +682,12 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
647682
raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})"
648683
end
649684
HALT
685+
elsif GraphQL::Execution::SKIP_FROM_PARENT_LIST == value
686+
unless selection_result.has_graphql_graph_parent_that_skips_list_items_that_raise
687+
raise "Cannot skip list items from lists not marked `skip_items_on_raise: true`"
688+
end
689+
690+
set_result(selection_result, result_name, GraphQL::Execution::SKIP_FROM_PARENT_LIST)
650691
else
651692
# What could this actually _be_? Anyhow,
652693
# preserve the default behavior of doing nothing with it.
@@ -693,6 +734,8 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
693734
#
694735
# @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later
695736
def continue_field(path, value, owner_type, field, current_type, ast_node, next_selections, is_non_null, owner_object, arguments, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
737+
# puts("current_type: #{current_type.to_type_signature}, skip_nodes_on_raise: #{current_type.skip_nodes_on_raise?} ")
738+
696739
if current_type.non_null?
697740
current_type = current_type.of_type
698741
is_non_null = true
@@ -776,10 +819,13 @@ def continue_field(path, value, owner_type, field, current_type, ast_node, next_
776819
end
777820
when "LIST"
778821
inner_type = current_type.of_type
822+
# puts("LIST: #{current_type}, #{current_type.skip_nodes_on_raise?.inspect}")
823+
# puts("Item type: #{inner_type}, #{inner_type.skip_nodes_on_raise?.inspect}")
779824
# This is true for objects, unions, and interfaces
780825
use_dataloader_job = !inner_type.unwrap.kind.input?
781826
response_list = GraphQLResultArray.new(result_name, selection_result)
782827
response_list.graphql_non_null_list_items = inner_type.non_null?
828+
response_list.graphql_skip_list_items_that_raise = current_type.skip_nodes_on_raise?
783829
set_result(selection_result, result_name, response_list)
784830
result_was_set = false
785831
idx = 0

lib/graphql/query/context.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ def skip
1111
GraphQL::Execution::SKIP
1212
end
1313

14+
# Return this value to tell the runtime
15+
# to exclude this whole object from parent list.
16+
#
17+
# The runtime will find the find the nearest parent list marked `skip_items_on_raise: true`,
18+
# and exclude the entire list item (including this object).
19+
def skip_from_parent_list
20+
GraphQL::Execution::SKIP_FROM_PARENT_LIST
21+
end
22+
1423
# Add error at query-level.
1524
# @param error [GraphQL::ExecutionError] an execution error
1625
# @return [void]

lib/graphql/schema/build_from_definition.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ def build_resolve_type(lookup_hash, directives, missing_type_handler)
466466
when GraphQL::Language::Nodes::NonNullType
467467
resolve_type_proc.call(ast_node.of_type).to_non_null_type
468468
when GraphQL::Language::Nodes::ListType
469-
resolve_type_proc.call(ast_node.of_type).to_list_type
469+
resolve_type_proc.call(ast_node.of_type).to_list_type(skip_nodes_on_raise: false)
470470
when String
471471
directives[ast_node]
472472
else

lib/graphql/schema/field.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def method_conflict_warning?
219219
# @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
220220
# @param validates [Array<Hash>] Configurations for validating this field
221221
# @fallback_value [Object] A fallback value if the method is not defined
222-
def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_given, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: :not_given, default_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: :not_given, &definition_block)
222+
def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_given, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: :not_given, default_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: :not_given, skip_nodes_on_raise: false, &definition_block)
223223
if name.nil?
224224
raise ArgumentError, "missing first `name` argument or keyword `name:`"
225225
end
@@ -271,6 +271,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_gi
271271
else
272272
true
273273
end
274+
@skip_nodes_on_raise = skip_nodes_on_raise
274275
@connection = connection
275276
@has_max_page_size = max_page_size != :not_given
276277
@max_page_size = max_page_size == :not_given ? nil : max_page_size
@@ -349,6 +350,10 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: :not_gi
349350

350351
self.extensions.each(&:after_define_apply)
351352
@call_after_define = true
353+
354+
if skip_nodes_on_raise && !self.type.list?
355+
raise ArgumentError, "The `skip_nodes_on_raise` option is only applicable to lists."
356+
end
352357
end
353358

354359
# If true, subscription updates with this field can be shared between viewers
@@ -572,9 +577,10 @@ def type
572577
raise MissingReturnTypeError, "Can't determine the return type for #{self.path} (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)"
573578
end
574579
nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
580+
# TODO: Pull `skip_nodes_on_raise` from resolver class
575581
Member::BuildType.parse_type(return_type, null: nullable)
576582
else
577-
@type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
583+
@type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null, skip_nodes_on_raise: @skip_nodes_on_raise)
578584
end
579585
rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
580586
# Let this propagate up

lib/graphql/schema/late_bound_type.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ def to_non_null_type
2121
@to_non_null_type ||= GraphQL::Schema::NonNull.new(self)
2222
end
2323

24-
def to_list_type
25-
@to_list_type ||= GraphQL::Schema::List.new(self)
24+
# can we just inherit this from graphql/schema/member/type_system_helpers.rb?
25+
def to_list_type(skip_nodes_on_raise: false)
26+
if skip_nodes_on_raise
27+
@to_skipping_list_type ||= GraphQL::Schema::List.new(self, skip_nodes_on_raise: true)
28+
else
29+
@to_list_type ||= GraphQL::Schema::List.new(self)
30+
end
2631
end
2732

2833
def inspect

lib/graphql/schema/list.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ class Schema
88
class List < GraphQL::Schema::Wrapper
99
include Schema::Member::ValidatesInput
1010

11+
def initialize(of_type, skip_nodes_on_raise: false)
12+
super(of_type)
13+
@skip_nodes_on_raise = skip_nodes_on_raise
14+
end
15+
1116
# @return [GraphQL::TypeKinds::LIST]
1217
def kind
1318
GraphQL::TypeKinds::LIST
@@ -18,6 +23,10 @@ def list?
1823
true
1924
end
2025

26+
def skip_nodes_on_raise?
27+
@skip_nodes_on_raise
28+
end
29+
2130
def to_type_signature
2231
"[#{@of_type.to_type_signature}]"
2332
end

lib/graphql/schema/member/build_type.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module BuildType
99
module_function
1010
# @param type_expr [String, Class, GraphQL::BaseType]
1111
# @return [GraphQL::BaseType]
12-
def parse_type(type_expr, null:)
12+
def parse_type(type_expr, null:, skip_nodes_on_raise: false)
1313
list_type = false
1414

1515
return_type = case type_expr
@@ -85,7 +85,7 @@ def parse_type(type_expr, null:)
8585
# Apply list_type first, that way the
8686
# .to_non_null_type applies to the list type, not the inner type
8787
if list_type
88-
return_type = return_type.to_list_type
88+
return_type = return_type.to_list_type(skip_nodes_on_raise: skip_nodes_on_raise)
8989
end
9090

9191
if !null

lib/graphql/schema/member/type_system_helpers.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@ def to_non_null_type
1717
end
1818

1919
# @return [Schema::List] Make a list-type representation of this type
20-
def to_list_type
21-
@to_list_type ||= GraphQL::Schema::List.new(self)
20+
def to_list_type(skip_nodes_on_raise: false)
21+
if skip_nodes_on_raise
22+
@to_skipping_list_type ||= GraphQL::Schema::List.new(self, skip_nodes_on_raise: true)
23+
else
24+
@to_list_type ||= GraphQL::Schema::List.new(self)
25+
end
2226
end
2327

2428
# @return [Boolean] true if this is a non-nullable type. A nullable list of non-nullables is considered nullable.
2529
def non_null?
2630
false
2731
end
2832

33+
# @return [Boolean] true if this field's nodes should be skipped, if resolving them led to an error being raised.
34+
def skip_nodes_on_raise?
35+
false
36+
end
37+
2938
# @return [Boolean] true if this is a list type. A non-nullable list is considered a list.
3039
def list?
3140
false

lib/graphql/schema/non_null.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ def non_null?
1818
true
1919
end
2020

21+
# @return [Boolean] true if this field's nodes should be skipped, if resolving them led to an error being raised.
22+
def skip_nodes_on_raise?
23+
@of_type.skip_nodes_on_raise?
24+
end
25+
2126
# @return [Boolean] True if this type wraps a list type
2227
def list?
2328
@of_type.list?
@@ -27,6 +32,10 @@ def to_type_signature
2732
"#{@of_type.to_type_signature}!"
2833
end
2934

35+
def to_s
36+
inspect
37+
end
38+
3039
def inspect
3140
"#<#{self.class.name} @of_type=#{@of_type.inspect}>"
3241
end

0 commit comments

Comments
 (0)