Skip to content

Commit 6315fcb

Browse files
committed
Implement ability to skip list items on raise
1 parent e43152a commit 6315fcb

File tree

13 files changed

+378
-21
lines changed

13 files changed

+378
-21
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: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ def initialize(result_name, parent_result)
3030
attr_accessor :graphql_non_null_field_names
3131
# @return [nil, true]
3232
attr_accessor :graphql_non_null_list_items
33+
# True when this result is a List that was marked `skip_items_on_raise: true`.
34+
# Descendants of this result will have `can_be_skipped?` be true.
35+
# @return [nil, true]
36+
attr_accessor :graphql_skip_list_items_that_raise
37+
38+
# A result can be skipped when it's an ancestor of an item in a List marked `skip_items_on_raise: true`.
39+
# @return [Boolean]
40+
def can_be_skipped?
41+
return @can_be_skipped if defined?(@can_be_skipped)
42+
43+
@can_be_skipped = graphql_skip_list_items_that_raise || !!(graphql_parent && graphql_parent.can_be_skipped?)
44+
end
3345

3446
# @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
3547
attr_accessor :graphql_result_data
@@ -513,10 +525,14 @@ def evaluate_selection_with_args(arguments, field_defn, next_path, ast_node, fie
513525
rescue GraphQL::ExecutionError => err
514526
err
515527
rescue StandardError => err
516-
begin
517-
query.handle_or_reraise(err)
518-
rescue GraphQL::ExecutionError => ex_err
519-
ex_err
528+
if selection_result.can_be_skipped?
529+
context.skip_from_parent_list # Skip silently without informing the user's `rescue_from` block.
530+
else
531+
begin
532+
query.handle_or_reraise(err)
533+
rescue GraphQL::ExecutionError => ex_err
534+
ex_err
535+
end
520536
end
521537
end
522538
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|
@@ -566,6 +582,19 @@ def set_result(selection_result, result_name, value)
566582
set_result(parent, name_in_parent, nil)
567583
set_graphql_dead(selection_result)
568584
end
585+
elsif value == GraphQL::Execution::SKIP_FROM_PARENT_LIST
586+
if selection_result.graphql_skip_list_items_that_raise # This is the first list that this item can be skipped from.
587+
unless selection_result.is_a?(GraphQLResultArray)
588+
raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})"
589+
end
590+
591+
selection_result.graphql_skip_at(result_name)
592+
else # Propograte up to find the first list this item can be skipped from.
593+
parent = selection_result.graphql_parent
594+
name_in_parent = selection_result.graphql_result_name
595+
set_result(parent, name_in_parent, GraphQL::Execution::SKIP_FROM_PARENT_LIST)
596+
set_graphql_dead(selection_result)
597+
end
569598
else
570599
selection_result[result_name] = value
571600
end
@@ -647,6 +676,12 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
647676
raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})"
648677
end
649678
HALT
679+
elsif GraphQL::Execution::SKIP_FROM_PARENT_LIST == value
680+
unless selection_result.can_be_skipped?
681+
raise "Cannot skip list items from lists not marked `skip_items_on_raise: true`"
682+
end
683+
684+
set_result(selection_result, result_name, GraphQL::Execution::SKIP_FROM_PARENT_LIST)
650685
else
651686
# What could this actually _be_? Anyhow,
652687
# preserve the default behavior of doing nothing with it.
@@ -780,6 +815,7 @@ def continue_field(path, value, owner_type, field, current_type, ast_node, next_
780815
use_dataloader_job = !inner_type.unwrap.kind.input?
781816
response_list = GraphQLResultArray.new(result_name, selection_result)
782817
response_list.graphql_non_null_list_items = inner_type.non_null?
818+
response_list.graphql_skip_list_items_that_raise = current_type.skip_nodes_on_raise?
783819
set_result(selection_result, result_name, response_list)
784820
result_was_set = false
785821
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: 10 additions & 3 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,11 @@ 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+
# TODO: should this check be centralized in `GraphQL::Schema::Member::BuildType#parse_type` instead?
355+
if skip_nodes_on_raise && !self.type.list?
356+
raise ArgumentError, "The `skip_nodes_on_raise` option is only applicable to lists."
357+
end
352358
end
353359

354360
# If true, subscription updates with this field can be shared between viewers
@@ -572,9 +578,10 @@ def type
572578
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)"
573579
end
574580
nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
575-
Member::BuildType.parse_type(return_type, null: nullable)
581+
skip_nodes_on_raise = @skip_nodes_on_raise.nil? ? @resolver_class.skip_nodes_on_raise : @skip_nodes_on_raise
582+
Member::BuildType.parse_type(return_type, null: nullable, skip_nodes_on_raise: skip_nodes_on_raise)
576583
else
577-
@type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
584+
@type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null, skip_nodes_on_raise: @skip_nodes_on_raise)
578585
end
579586
rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
580587
# 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: 5 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?

0 commit comments

Comments
 (0)