Skip to content

Commit 3ff5afa

Browse files
committed
wip
1 parent f88ec1f commit 3ff5afa

File tree

9 files changed

+100
-16
lines changed

9 files changed

+100
-16
lines changed

lib/graphql/execution/interpreter/runtime.rb

Lines changed: 50 additions & 6 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+
nil
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
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)
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
@@ -591,7 +626,11 @@ def set_graphql_dead(selection_result)
591626
def continue_value(path, value, parent_type, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
592627
case value
593628
when nil
594-
if is_non_null
629+
if selection_result.has_graphql_graph_parent_that_skips_list_items_that_raise
630+
# This writes the `nil` in, we need to patch it to skip instead.
631+
print("skip!")
632+
set_result(selection_result, result_name, GraphQL::Execution::SKIP)
633+
elsif is_non_null
595634
set_result(selection_result, result_name, nil) do
596635
# This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
597636
err = parent_type::InvalidNullError.new(parent_type, field, value)
@@ -693,6 +732,8 @@ def continue_value(path, value, parent_type, field, is_non_null, ast_node, resul
693732
#
694733
# @return [Lazy, Array, Hash, Object] Lazy, Array, and Hash are all traversed to resolve lazy values later
695734
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
735+
# puts("current_type: #{current_type.to_type_signature}, skip_nodes_on_raise: #{current_type.skip_nodes_on_raise?} ")
736+
696737
if current_type.non_null?
697738
current_type = current_type.of_type
698739
is_non_null = true
@@ -776,10 +817,13 @@ def continue_field(path, value, owner_type, field, current_type, ast_node, next_
776817
end
777818
when "LIST"
778819
inner_type = current_type.of_type
820+
puts("LIST: #{current_type}, #{current_type.skip_nodes_on_raise?.inspect}")
821+
puts("Item type: #{inner_type}, #{inner_type.skip_nodes_on_raise?.inspect}")
779822
# This is true for objects, unions, and interfaces
780823
use_dataloader_job = !inner_type.unwrap.kind.input?
781824
response_list = GraphQLResultArray.new(result_name, selection_result)
782825
response_list.graphql_non_null_list_items = inner_type.non_null?
826+
response_list.graphql_skip_list_items_that_raise = current_type.skip_nodes_on_raise?
783827
set_result(selection_result, result_name, response_list)
784828
result_was_set = false
785829
idx = 0

lib/graphql/schema/addition.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def update_type_owner(owner, type)
132132
end
133133
orig_type = orig_type.of_type
134134
end
135+
# This can't call to_list_type(skip_nodes_on_raise: ...), so I'll default the arg instead
135136
transforms.reverse_each { |t| type = type.public_send(t) }
136137
end
137138
owner.type = type

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: 4 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
@@ -572,9 +573,10 @@ def type
572573
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)"
573574
end
574575
nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
576+
# TODO: Pull `skip_nodes_on_raise` from resolver class
575577
Member::BuildType.parse_type(return_type, null: nullable)
576578
else
577-
@type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
579+
@type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null, skip_nodes_on_raise: @skip_nodes_on_raise)
578580
end
579581
rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
580582
# Let this propagate up

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: 7 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,8 @@ def to_type_signature
2732
"#{@of_type.to_type_signature}!"
2833
end
2934

35+
def to_s = inspect
36+
3037
def inspect
3138
"#<#{self.class.name} @of_type=#{@of_type.inspect}>"
3239
end

lib/graphql/types/relay/connection_behaviors.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def self.included(child_class)
1313
child_class.default_relay(true)
1414
child_class.has_nodes_field(true)
1515
child_class.node_nullable(true)
16+
child_class.skip_nodes_on_raise(false)
1617
child_class.edges_nullable(true)
1718
child_class.edge_nullable(true)
1819
add_page_info_field(child_class)
@@ -36,7 +37,7 @@ module ClassMethods
3637
# class name to set defaults. You can call it again in the class definition
3738
# to override the default (or provide a value, if the default lookup failed).
3839
# @param field_options [Hash] Any extra keyword arguments to pass to the `field :edges, ...` and `field :nodes, ...` configurations
39-
def edge_type(edge_type_class, edge_class: GraphQL::Pagination::Connection::Edge, node_type: edge_type_class.node_type, nodes_field: self.has_nodes_field, node_nullable: self.node_nullable, edges_nullable: self.edges_nullable, edge_nullable: self.edge_nullable, field_options: nil)
40+
def edge_type(edge_type_class, edge_class: GraphQL::Pagination::Connection::Edge, node_type: edge_type_class.node_type, nodes_field: self.has_nodes_field, node_nullable: self.node_nullable, skip_nodes_on_raise: self.skip_nodes_on_raise, edges_nullable: self.edges_nullable, edge_nullable: self.edge_nullable, field_options: nil)
4041
# Set this connection's graphql name
4142
node_type_name = node_type.graphql_name
4243

@@ -60,7 +61,7 @@ def edge_type(edge_type_class, edge_class: GraphQL::Pagination::Connection::Edge
6061

6162
field(**base_field_options)
6263

63-
define_nodes_field(node_nullable, field_options: field_options) if nodes_field
64+
define_nodes_field(node_nullable, skip_nodes_on_raise, field_options: field_options) if nodes_field
6465

6566
description("The connection type for #{node_type_name}.")
6667
end
@@ -88,12 +89,22 @@ def visible?(ctx)
8889
# Use `node_nullable(false)` in your base class to make non-null `node` and `nodes` fields.
8990
def node_nullable(new_value = nil)
9091
if new_value.nil?
92+
# p([self, superclass, superclass.respond_to?(:node_nullable), superclass.try(:node_nullable)])
9193
defined?(@node_nullable) ? @node_nullable : superclass.node_nullable
9294
else
9395
@node_nullable = new_value
9496
end
9597
end
9698

99+
def skip_nodes_on_raise(new_value = nil)
100+
if new_value.nil?
101+
# p([self, superclass, superclass.respond_to?(:skip_nodes_on_raise), superclass.try(:skip_nodes_on_raise)])
102+
defined?(@skip_nodes_on_raise) ? @skip_nodes_on_raise : superclass.skip_nodes_on_raise
103+
else
104+
@skip_nodes_on_raise = new_value
105+
end
106+
end
107+
97108
# Set the default `edges_nullable` for this class and its child classes. (Defaults to `true`.)
98109
# Use `edges_nullable(false)` in your base class to make non-null `edges` fields.
99110
def edges_nullable(new_value = nil)
@@ -126,7 +137,7 @@ def has_nodes_field(new_value = nil)
126137

127138
private
128139

129-
def define_nodes_field(nullable, field_options: nil)
140+
def define_nodes_field(nullable, skip_nodes_on_raise, field_options: nil)
130141
base_field_options = {
131142
name: :nodes,
132143
type: [@node_type, null: nullable],
@@ -135,6 +146,7 @@ def define_nodes_field(nullable, field_options: nil)
135146
connection: false,
136147
# Assume that the connection was scoped before this step:
137148
scope: false,
149+
skip_nodes_on_raise: skip_nodes_on_raise,
138150
}
139151
if field_options
140152
base_field_options.merge!(field_options)

0 commit comments

Comments
 (0)