graphql 2.5.11 → 2.5.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
- data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
- data/lib/graphql/dashboard.rb +5 -2
- data/lib/graphql/dataloader/async_dataloader.rb +22 -11
- data/lib/graphql/dataloader/null_dataloader.rb +44 -10
- data/lib/graphql/dataloader.rb +75 -23
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/execution/interpreter/resolve.rb +7 -13
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
- data/lib/graphql/execution/interpreter/runtime.rb +21 -16
- data/lib/graphql/execution/interpreter.rb +2 -13
- data/lib/graphql/language/document_from_schema_definition.rb +2 -1
- data/lib/graphql/language.rb +21 -12
- data/lib/graphql/schema/argument.rb +7 -0
- data/lib/graphql/schema/build_from_definition.rb +3 -1
- data/lib/graphql/schema/directive.rb +22 -4
- data/lib/graphql/schema/field.rb +6 -47
- data/lib/graphql/schema/member/has_arguments.rb +43 -14
- data/lib/graphql/schema/member/has_fields.rb +76 -4
- data/lib/graphql/schema/validator/required_validator.rb +33 -2
- data/lib/graphql/schema/visibility.rb +2 -2
- data/lib/graphql/schema.rb +20 -3
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +2 -2
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
- data/lib/graphql/testing/helpers.rb +12 -9
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
- data/lib/graphql/tracing/detailed_trace.rb +70 -7
- data/lib/graphql/tracing/perfetto_trace.rb +208 -78
- data/lib/graphql/tracing/sentry_trace.rb +3 -1
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +5 -2
- metadata +7 -3
|
@@ -35,11 +35,10 @@ module GraphQL
|
|
|
35
35
|
# @return [GraphQL::Query::Context]
|
|
36
36
|
attr_reader :context
|
|
37
37
|
|
|
38
|
-
def initialize(query
|
|
38
|
+
def initialize(query:)
|
|
39
39
|
@query = query
|
|
40
40
|
@current_trace = query.current_trace
|
|
41
41
|
@dataloader = query.multiplex.dataloader
|
|
42
|
-
@lazies_at_depth = lazies_at_depth
|
|
43
42
|
@schema = query.schema
|
|
44
43
|
@context = query.context
|
|
45
44
|
@response = nil
|
|
@@ -365,6 +364,10 @@ module GraphQL
|
|
|
365
364
|
else
|
|
366
365
|
@query.arguments_cache.dataload_for(ast_node, field_defn, owner_object) do |resolved_arguments|
|
|
367
366
|
runtime_state = get_current_runtime_state # This might be in a different fiber
|
|
367
|
+
runtime_state.current_field = field_defn
|
|
368
|
+
runtime_state.current_arguments = resolved_arguments
|
|
369
|
+
runtime_state.current_result_name = result_name
|
|
370
|
+
runtime_state.current_result = selections_result
|
|
368
371
|
evaluate_selection_with_args(resolved_arguments, field_defn, ast_node, field_ast_nodes, owner_object, result_name, selections_result, runtime_state)
|
|
369
372
|
end
|
|
370
373
|
end
|
|
@@ -373,6 +376,8 @@ module GraphQL
|
|
|
373
376
|
def evaluate_selection_with_args(arguments, field_defn, ast_node, field_ast_nodes, object, result_name, selection_result, runtime_state) # rubocop:disable Metrics/ParameterLists
|
|
374
377
|
after_lazy(arguments, field: field_defn, ast_node: ast_node, owner_object: object, arguments: arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |resolved_arguments, runtime_state|
|
|
375
378
|
if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
|
|
379
|
+
next if selection_result.collect_result(result_name, resolved_arguments)
|
|
380
|
+
|
|
376
381
|
return_type_non_null = field_defn.type.non_null?
|
|
377
382
|
continue_value(resolved_arguments, field_defn, return_type_non_null, ast_node, result_name, selection_result)
|
|
378
383
|
next
|
|
@@ -446,7 +451,7 @@ module GraphQL
|
|
|
446
451
|
}
|
|
447
452
|
end
|
|
448
453
|
|
|
449
|
-
|
|
454
|
+
call_method_on_directives(:resolve, object, directives) do
|
|
450
455
|
if !directives.empty?
|
|
451
456
|
# This might be executed in a different context; reset this info
|
|
452
457
|
runtime_state = get_current_runtime_state
|
|
@@ -472,6 +477,8 @@ module GraphQL
|
|
|
472
477
|
end
|
|
473
478
|
@current_trace.end_execute_field(field_defn, object, kwarg_arguments, query, app_result)
|
|
474
479
|
after_lazy(app_result, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |inner_result, runtime_state|
|
|
480
|
+
next if selection_result.collect_result(result_name, inner_result)
|
|
481
|
+
|
|
475
482
|
owner_type = selection_result.graphql_result_type
|
|
476
483
|
return_type = field_defn.type
|
|
477
484
|
continue_value = continue_value(inner_result, field_defn, return_type.non_null?, ast_node, result_name, selection_result)
|
|
@@ -488,7 +495,7 @@ module GraphQL
|
|
|
488
495
|
# all of its child fields before moving on to the next root mutation field.
|
|
489
496
|
# (Subselections of this mutation will still be resolved level-by-level.)
|
|
490
497
|
if selection_result.graphql_is_eager
|
|
491
|
-
|
|
498
|
+
@dataloader.run
|
|
492
499
|
end
|
|
493
500
|
end
|
|
494
501
|
|
|
@@ -667,7 +674,11 @@ module GraphQL
|
|
|
667
674
|
rescue GraphQL::ExecutionError => ex_err
|
|
668
675
|
return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result)
|
|
669
676
|
rescue StandardError => err
|
|
670
|
-
|
|
677
|
+
begin
|
|
678
|
+
query.handle_or_reraise(err)
|
|
679
|
+
rescue GraphQL::ExecutionError => ex_err
|
|
680
|
+
return continue_value(ex_err, field, is_non_null, ast_node, result_name, selection_result)
|
|
681
|
+
end
|
|
671
682
|
end
|
|
672
683
|
set_result(selection_result, result_name, r, false, is_non_null)
|
|
673
684
|
r
|
|
@@ -879,7 +890,6 @@ module GraphQL
|
|
|
879
890
|
# @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
|
|
880
891
|
def after_lazy(lazy_obj, field:, owner_object:, arguments:, ast_node:, result:, result_name:, eager: false, runtime_state:, trace: true, &block)
|
|
881
892
|
if lazy?(lazy_obj)
|
|
882
|
-
orig_result = result
|
|
883
893
|
was_authorized_by_scope_items = runtime_state.was_authorized_by_scope_items
|
|
884
894
|
lazy = GraphQL::Execution::Lazy.new(field: field) do
|
|
885
895
|
# This block might be called in a new fiber;
|
|
@@ -889,13 +899,13 @@ module GraphQL
|
|
|
889
899
|
runtime_state.current_field = field
|
|
890
900
|
runtime_state.current_arguments = arguments
|
|
891
901
|
runtime_state.current_result_name = result_name
|
|
892
|
-
runtime_state.current_result =
|
|
902
|
+
runtime_state.current_result = result
|
|
893
903
|
runtime_state.was_authorized_by_scope_items = was_authorized_by_scope_items
|
|
894
904
|
# Wrap the execution of _this_ method with tracing,
|
|
895
905
|
# but don't wrap the continuation below
|
|
896
|
-
|
|
906
|
+
sync_result = nil
|
|
897
907
|
inner_obj = begin
|
|
898
|
-
|
|
908
|
+
sync_result = if trace
|
|
899
909
|
@current_trace.begin_execute_field(field, owner_object, arguments, query)
|
|
900
910
|
@current_trace.execute_field_lazy(field: field, query: query, object: owner_object, arguments: arguments, ast_node: ast_node) do
|
|
901
911
|
schema.sync_lazy(lazy_obj)
|
|
@@ -913,7 +923,7 @@ module GraphQL
|
|
|
913
923
|
end
|
|
914
924
|
ensure
|
|
915
925
|
if trace
|
|
916
|
-
@current_trace.end_execute_field(field, owner_object, arguments, query,
|
|
926
|
+
@current_trace.end_execute_field(field, owner_object, arguments, query, sync_result)
|
|
917
927
|
end
|
|
918
928
|
end
|
|
919
929
|
yield(inner_obj, runtime_state)
|
|
@@ -923,12 +933,7 @@ module GraphQL
|
|
|
923
933
|
lazy.value
|
|
924
934
|
else
|
|
925
935
|
set_result(result, result_name, lazy, false, false) # is_non_null is irrelevant here
|
|
926
|
-
|
|
927
|
-
while result
|
|
928
|
-
current_depth += 1
|
|
929
|
-
result = result.graphql_parent
|
|
930
|
-
end
|
|
931
|
-
@lazies_at_depth[current_depth] << lazy
|
|
936
|
+
@dataloader.lazy_at_depth(result.depth, lazy)
|
|
932
937
|
lazy
|
|
933
938
|
end
|
|
934
939
|
else
|
|
@@ -42,7 +42,6 @@ module GraphQL
|
|
|
42
42
|
trace.execute_multiplex(multiplex: multiplex) do
|
|
43
43
|
schema = multiplex.schema
|
|
44
44
|
queries = multiplex.queries
|
|
45
|
-
lazies_at_depth = Hash.new { |h, k| h[k] = [] }
|
|
46
45
|
multiplex_analyzers = schema.multiplex_analyzers
|
|
47
46
|
if multiplex.max_complexity
|
|
48
47
|
multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity]
|
|
@@ -73,7 +72,7 @@ module GraphQL
|
|
|
73
72
|
# Although queries in a multiplex _share_ an Interpreter instance,
|
|
74
73
|
# they also have another item of state, which is private to that query
|
|
75
74
|
# in particular, assign it here:
|
|
76
|
-
runtime = Runtime.new(query: query
|
|
75
|
+
runtime = Runtime.new(query: query)
|
|
77
76
|
query.context.namespace(:interpreter_runtime)[:runtime] = runtime
|
|
78
77
|
|
|
79
78
|
query.current_trace.execute_query(query: query) do
|
|
@@ -81,23 +80,13 @@ module GraphQL
|
|
|
81
80
|
end
|
|
82
81
|
rescue GraphQL::ExecutionError => err
|
|
83
82
|
query.context.errors << err
|
|
84
|
-
NO_OPERATION
|
|
85
83
|
end
|
|
86
84
|
end
|
|
87
85
|
results[idx] = result
|
|
88
86
|
}
|
|
89
87
|
end
|
|
90
88
|
|
|
91
|
-
multiplex.dataloader.run
|
|
92
|
-
|
|
93
|
-
# Then, work through lazy results in a breadth-first way
|
|
94
|
-
multiplex.dataloader.append_job {
|
|
95
|
-
query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil
|
|
96
|
-
multiplex.current_trace.execute_query_lazy(multiplex: multiplex, query: query) do
|
|
97
|
-
Interpreter::Resolve.resolve_each_depth(lazies_at_depth, multiplex.dataloader)
|
|
98
|
-
end
|
|
99
|
-
}
|
|
100
|
-
multiplex.dataloader.run
|
|
89
|
+
multiplex.dataloader.run(trace_query_lazy: multiplex)
|
|
101
90
|
|
|
102
91
|
# Then, find all errors and assign the result to the query object
|
|
103
92
|
results.each_with_index do |data_result, idx|
|
|
@@ -52,8 +52,9 @@ module GraphQL
|
|
|
52
52
|
|
|
53
53
|
def build_object_type_node(object_type)
|
|
54
54
|
ints = @types.interfaces(object_type)
|
|
55
|
+
|
|
55
56
|
if !ints.empty?
|
|
56
|
-
ints.sort_by
|
|
57
|
+
ints = ints.sort_by(&:graphql_name)
|
|
57
58
|
ints.map! { |iface| build_type_name_node(iface) }
|
|
58
59
|
end
|
|
59
60
|
|
data/lib/graphql/language.rb
CHANGED
|
@@ -77,21 +77,30 @@ module GraphQL
|
|
|
77
77
|
new_query_str || query_str
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
LEADING_REGEX = Regexp.union(" ", *Lexer::Punctuation.constants.map { |const| Lexer::Punctuation.const_get(const) })
|
|
81
|
+
|
|
82
|
+
# Optimized pattern using:
|
|
83
|
+
# - Possessive quantifiers (*+, ++) to prevent backtracking in number patterns
|
|
84
|
+
# - Atomic group (?>...) for IGNORE to prevent backtracking
|
|
85
|
+
# - Single unified number pattern instead of three alternatives
|
|
86
|
+
EFFICIENT_NUMBER_REGEXP = /-?(?:0|[1-9][0-9]*+)(?:\.[0-9]++)?(?:[eE][+-]?[0-9]++)?/
|
|
87
|
+
EFFICIENT_IGNORE_REGEXP = /(?>[, \r\n\t]+|\#[^\n]*$)*/
|
|
88
|
+
|
|
89
|
+
MAYBE_INVALID_NUMBER = /\d[_a-zA-Z]/
|
|
90
|
+
|
|
80
91
|
INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP = %r{
|
|
81
|
-
(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)}x
|
|
92
|
+
(?<leading>#{LEADING_REGEX})
|
|
93
|
+
(?<num>#{EFFICIENT_NUMBER_REGEXP})
|
|
94
|
+
(?<name>#{Lexer::IDENTIFIER_REGEXP})
|
|
95
|
+
#{EFFICIENT_IGNORE_REGEXP}
|
|
96
|
+
:
|
|
97
|
+
}x
|
|
88
98
|
|
|
89
99
|
def self.add_space_between_numbers_and_names(query_str)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
end
|
|
100
|
+
# Fast check for digit followed by identifier char. If this doesn't match, skip the more expensive regexp entirely.
|
|
101
|
+
return query_str unless query_str.match?(MAYBE_INVALID_NUMBER)
|
|
102
|
+
return query_str unless query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
|
|
103
|
+
query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<leading>\\k<num> \\k<name>:")
|
|
95
104
|
end
|
|
96
105
|
end
|
|
97
106
|
end
|
|
@@ -39,9 +39,14 @@ module GraphQL
|
|
|
39
39
|
# @param arg_name [Symbol]
|
|
40
40
|
# @param type_expr
|
|
41
41
|
# @param desc [String]
|
|
42
|
+
# @param type [Class, Array<Class>] Input type; positional argument also accepted
|
|
43
|
+
# @param name [Symbol] positional argument also accepted # @param loads [Class, Array<Class>] A GraphQL type to load for the given ID when one is present
|
|
44
|
+
# @param definition_block [Proc] Called with the newly-created {Argument}
|
|
45
|
+
# @param owner [Class] Private, used by GraphQL-Ruby during schema definition
|
|
42
46
|
# @param required [Boolean, :nullable] if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`.
|
|
43
47
|
# @param description [String]
|
|
44
48
|
# @param default_value [Object]
|
|
49
|
+
# @param loads [Class, Array<Class>] A GraphQL type to load for the given ID when one is present
|
|
45
50
|
# @param as [Symbol] Override the keyword name when passed to a method
|
|
46
51
|
# @param prepare [Symbol] A method to call to transform this argument's valuebefore sending it to field resolution
|
|
47
52
|
# @param camelize [Boolean] if true, the name will be camelized when building the schema
|
|
@@ -50,6 +55,8 @@ module GraphQL
|
|
|
50
55
|
# @param deprecation_reason [String]
|
|
51
56
|
# @param validates [Hash, nil] Options for building validators, if any should be applied
|
|
52
57
|
# @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value`
|
|
58
|
+
# @param comment [String] Private, used by GraphQL-Ruby when parsing GraphQL schema files
|
|
59
|
+
# @param ast_node [GraphQL::Language::Nodes::InputValueDefinition] Private, used by GraphQL-Ruby when parsing schema files
|
|
53
60
|
def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, comment: nil, ast_node: nil, default_value: NOT_CONFIGURED, as: nil, from_resolver: false, camelize: true, prepare: nil, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block)
|
|
54
61
|
arg_name ||= name
|
|
55
62
|
@name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
|
|
@@ -266,6 +266,8 @@ module GraphQL
|
|
|
266
266
|
build_scalar_type(definition, type_resolver, base_types[:scalar], default_resolve: default_resolve)
|
|
267
267
|
when GraphQL::Language::Nodes::InputObjectTypeDefinition
|
|
268
268
|
build_input_object_type(definition, type_resolver, base_types[:input_object])
|
|
269
|
+
when GraphQL::Language::Nodes::DirectiveDefinition
|
|
270
|
+
build_directive(definition, type_resolver)
|
|
269
271
|
end
|
|
270
272
|
end
|
|
271
273
|
|
|
@@ -544,7 +546,7 @@ module GraphQL
|
|
|
544
546
|
when GraphQL::Language::Nodes::ListType
|
|
545
547
|
resolve_type_proc.call(ast_node.of_type).to_list_type
|
|
546
548
|
when String
|
|
547
|
-
directives[ast_node]
|
|
549
|
+
directives[ast_node] ||= missing_type_handler.call(ast_node)
|
|
548
550
|
else
|
|
549
551
|
raise "Unexpected ast_node: #{ast_node.inspect}"
|
|
550
552
|
end
|
|
@@ -129,11 +129,29 @@ module GraphQL
|
|
|
129
129
|
# not runtime arguments.
|
|
130
130
|
context = Query::NullContext.instance
|
|
131
131
|
self.class.all_argument_definitions.each do |arg_defn|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
keyword = arg_defn.keyword
|
|
133
|
+
arg_type = arg_defn.type
|
|
134
|
+
if arguments.key?(keyword)
|
|
135
|
+
value = arguments[keyword]
|
|
134
136
|
# This is a Ruby-land value; convert it to graphql for validation
|
|
135
137
|
graphql_value = begin
|
|
136
|
-
|
|
138
|
+
coerce_value = value
|
|
139
|
+
if arg_type.list? && (!coerce_value.nil?) && (!coerce_value.is_a?(Array))
|
|
140
|
+
# When validating inputs, GraphQL accepts a single item
|
|
141
|
+
# and implicitly converts it to a one-item list.
|
|
142
|
+
# However, we're using result coercion here to go from Ruby value
|
|
143
|
+
# to GraphQL value, so it doesn't have that feature.
|
|
144
|
+
# Keep the GraphQL-type behavior but implement it manually:
|
|
145
|
+
wrap_type = arg_type
|
|
146
|
+
while wrap_type.list?
|
|
147
|
+
if wrap_type.non_null?
|
|
148
|
+
wrap_type = wrap_type.of_type
|
|
149
|
+
end
|
|
150
|
+
wrap_type = wrap_type.of_type
|
|
151
|
+
coerce_value = [coerce_value]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
arg_type.coerce_isolated_result(coerce_value)
|
|
137
155
|
rescue GraphQL::Schema::Enum::UnresolvedValueError
|
|
138
156
|
# Let validation handle this
|
|
139
157
|
value
|
|
@@ -142,7 +160,7 @@ module GraphQL
|
|
|
142
160
|
value = graphql_value = nil
|
|
143
161
|
end
|
|
144
162
|
|
|
145
|
-
result =
|
|
163
|
+
result = arg_type.validate_input(graphql_value, context)
|
|
146
164
|
if !result.valid?
|
|
147
165
|
raise InvalidArgumentError, "@#{graphql_name}.#{arg_defn.graphql_name} on #{owner.path} is invalid (#{value.inspect}): #{result.problems.first["explanation"]}"
|
|
148
166
|
end
|
data/lib/graphql/schema/field.rb
CHANGED
|
@@ -109,52 +109,6 @@ module GraphQL
|
|
|
109
109
|
end
|
|
110
110
|
attr_writer :subscription_scope
|
|
111
111
|
|
|
112
|
-
# Create a field instance from a list of arguments, keyword arguments, and a block.
|
|
113
|
-
#
|
|
114
|
-
# This method implements prioritization between the `resolver` or `mutation` defaults
|
|
115
|
-
# and the local overrides via other keywords.
|
|
116
|
-
#
|
|
117
|
-
# It also normalizes positional arguments into keywords for {Schema::Field#initialize}.
|
|
118
|
-
# @param resolver [Class] A {GraphQL::Schema::Resolver} class to use for field configuration
|
|
119
|
-
# @param mutation [Class] A {GraphQL::Schema::Mutation} class to use for field configuration
|
|
120
|
-
# @param subscription [Class] A {GraphQL::Schema::Subscription} class to use for field configuration
|
|
121
|
-
# @return [GraphQL::Schema:Field] an instance of `self`
|
|
122
|
-
# @see {.initialize} for other options
|
|
123
|
-
def self.from_options(name = nil, type = nil, desc = nil, comment: nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block)
|
|
124
|
-
if (resolver_class = resolver || mutation || subscription)
|
|
125
|
-
# Add a reference to that parent class
|
|
126
|
-
kwargs[:resolver_class] = resolver_class
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
if name
|
|
130
|
-
kwargs[:name] = name
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
if comment
|
|
134
|
-
kwargs[:comment] = comment
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
if !type.nil?
|
|
138
|
-
if desc
|
|
139
|
-
if kwargs[:description]
|
|
140
|
-
raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc.inspect}, #{kwargs[:description].inspect})"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
kwargs[:description] = desc
|
|
144
|
-
kwargs[:type] = type
|
|
145
|
-
elsif (resolver || mutation) && type.is_a?(String)
|
|
146
|
-
# The return type should be copied from the resolver, and the second positional argument is the description
|
|
147
|
-
kwargs[:description] = type
|
|
148
|
-
else
|
|
149
|
-
kwargs[:type] = type
|
|
150
|
-
end
|
|
151
|
-
if type.is_a?(Class) && type < GraphQL::Schema::Mutation
|
|
152
|
-
raise ArgumentError, "Use `field #{name.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
new(**kwargs, &block)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
112
|
# Can be set with `connection: true|false` or inferred from a type name ending in `*Connection`
|
|
159
113
|
# @return [Boolean] if true, this field will be wrapped with Relay connection behavior
|
|
160
114
|
def connection?
|
|
@@ -255,6 +209,11 @@ module GraphQL
|
|
|
255
209
|
# @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
|
|
256
210
|
# @param validates [Array<Hash>] Configurations for validating this field
|
|
257
211
|
# @param fallback_value [Object] A fallback value if the method is not defined
|
|
212
|
+
# @param dynamic_introspection [Boolean] (Private, used by GraphQL-Ruby)
|
|
213
|
+
# @param relay_node_field [Boolean] (Private, used by GraphQL-Ruby)
|
|
214
|
+
# @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
|
|
215
|
+
# @param extras [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] Extra arguments to be injected into the resolver for this field
|
|
216
|
+
# @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
|
|
258
217
|
def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, 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: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
|
|
259
218
|
if name.nil?
|
|
260
219
|
raise ArgumentError, "missing first `name` argument or keyword `name:`"
|
|
@@ -347,7 +306,7 @@ module GraphQL
|
|
|
347
306
|
|
|
348
307
|
@extensions = EMPTY_ARRAY
|
|
349
308
|
@call_after_define = false
|
|
350
|
-
set_pagination_extensions(connection_extension: connection_extension)
|
|
309
|
+
set_pagination_extensions(connection_extension: NOT_CONFIGURED.equal?(connection_extension) ? self.class.connection_extension : connection_extension)
|
|
351
310
|
# Do this last so we have as much context as possible when initializing them:
|
|
352
311
|
if !extensions.empty?
|
|
353
312
|
self.extensions(extensions)
|
|
@@ -14,29 +14,52 @@ module GraphQL
|
|
|
14
14
|
cls.extend(ClassConfigured)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# @
|
|
18
|
-
# @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
# @param arg_name [Symbol] The underscore-cased name of this argument, `name:` keyword also accepted
|
|
18
|
+
# @param type_expr The GraphQL type of this argument; `type:` keyword also accepted
|
|
19
|
+
# @param desc [String] Argument description, `description:` keyword also accepted
|
|
20
|
+
# @option kwargs [Boolean, :nullable] :required if true, this argument is non-null; if false, this argument is nullable. If `:nullable`, then the argument must be provided, though it may be `null`.
|
|
21
|
+
# @option kwargs [String] :description Positional argument also accepted
|
|
22
|
+
# @option kwargs [Class, Array<Class>] :type Input type; positional argument also accepted
|
|
23
|
+
# @option kwargs [Symbol] :name positional argument also accepted
|
|
24
|
+
# @option kwargs [Object] :default_value
|
|
25
|
+
# @option kwargs [Class, Array<Class>] :loads A GraphQL type to load for the given ID when one is present
|
|
26
|
+
# @option kwargs [Symbol] :as Override the keyword name when passed to a method
|
|
27
|
+
# @option kwargs [Symbol] :prepare A method to call to transform this argument's valuebefore sending it to field resolution
|
|
28
|
+
# @option kwargs [Boolean] :camelize if true, the name will be camelized when building the schema
|
|
29
|
+
# @option kwargs [Boolean] :from_resolver if true, a Resolver class defined this argument
|
|
30
|
+
# @option kwargs [Hash{Class => Hash}] :directives
|
|
31
|
+
# @option kwargs [String] :deprecation_reason
|
|
32
|
+
# @option kwargs [String] :comment Private, used by GraphQL-Ruby when parsing GraphQL schema files
|
|
33
|
+
# @option kwargs [GraphQL::Language::Nodes::InputValueDefinition] :ast_node Private, used by GraphQL-Ruby when parsing schema files
|
|
34
|
+
# @option kwargs [Hash, nil] :validates Options for building validators, if any should be applied
|
|
35
|
+
# @option kwargs [Boolean] :replace_null_with_default if `true`, incoming values of `null` will be replaced with the configured `default_value`
|
|
36
|
+
# @param definition_block [Proc] Called with the newly-created {Argument}
|
|
37
|
+
# @param kwargs [Hash] Keywords for defining an argument. Any keywords not documented here must be handled by your base Argument class.
|
|
38
|
+
# @return [GraphQL::Schema::Argument] An instance of {argument_class} created from these arguments
|
|
39
|
+
def argument(arg_name = nil, type_expr = nil, desc = nil, **kwargs, &definition_block)
|
|
40
|
+
if kwargs[:loads]
|
|
41
|
+
loads_name = arg_name || kwargs[:name]
|
|
42
|
+
loads_name_as_string = loads_name.to_s
|
|
43
|
+
|
|
44
|
+
inferred_arg_name = case loads_name_as_string
|
|
27
45
|
when /_id$/
|
|
28
|
-
|
|
46
|
+
loads_name_as_string.sub(/_id$/, "").to_sym
|
|
29
47
|
when /_ids$/
|
|
30
|
-
|
|
48
|
+
loads_name_as_string.sub(/_ids$/, "")
|
|
31
49
|
.sub(/([^s])$/, "\\1s")
|
|
32
50
|
.to_sym
|
|
33
51
|
else
|
|
34
|
-
|
|
52
|
+
loads_name
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
kwargs[:as] ||= inferred_arg_name
|
|
38
56
|
end
|
|
39
|
-
|
|
57
|
+
kwargs[:owner] = self
|
|
58
|
+
arg_defn = self.argument_class.new(
|
|
59
|
+
arg_name, type_expr, desc,
|
|
60
|
+
**kwargs,
|
|
61
|
+
&definition_block
|
|
62
|
+
)
|
|
40
63
|
add_argument(arg_defn)
|
|
41
64
|
arg_defn
|
|
42
65
|
end
|
|
@@ -413,6 +436,12 @@ module GraphQL
|
|
|
413
436
|
end
|
|
414
437
|
end
|
|
415
438
|
|
|
439
|
+
# Called when an argument's `loads:` configuration fails to fetch an application object.
|
|
440
|
+
# By default, this method raises the given error, but you can override it to handle failures differently.
|
|
441
|
+
#
|
|
442
|
+
# @param err [GraphQL::LoadApplicationObjectFailedError] The error that occurred
|
|
443
|
+
# @return [Object, nil] If a value is returned, it will be used instead of the failed load
|
|
444
|
+
# @api public
|
|
416
445
|
def load_application_object_failed(err)
|
|
417
446
|
raise err
|
|
418
447
|
end
|
|
@@ -5,11 +5,83 @@ module GraphQL
|
|
|
5
5
|
class Member
|
|
6
6
|
# Shared code for Objects, Interfaces, Mutations, Subscriptions
|
|
7
7
|
module HasFields
|
|
8
|
+
include EmptyObjects
|
|
8
9
|
# Add a field to this object or interface with the given definition
|
|
9
|
-
# @
|
|
10
|
+
# @param name_positional [Symbol] The underscore-cased version of this field name (will be camelized for the GraphQL API); `name:` keyword is also accepted
|
|
11
|
+
# @param type_positional [Class, GraphQL::BaseType, Array] The return type of this field; `type:` keyword is also accepted
|
|
12
|
+
# @param desc_positional [String] Field description; `description:` keyword is also accepted
|
|
13
|
+
# @option kwargs [Symbol] :name The underscore-cased version of this field name (will be camelized for the GraphQL API); positional argument also accepted
|
|
14
|
+
# @option kwargs [Class, GraphQL::BaseType, Array] :type The return type of this field; positional argument is also accepted
|
|
15
|
+
# @option kwargs [Boolean] :null (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
|
|
16
|
+
# @option kwargs [String] :description Field description; positional argument also accepted
|
|
17
|
+
# @option kwargs [String] :comment Field comment
|
|
18
|
+
# @option kwargs [String] :deprecation_reason If present, the field is marked "deprecated" with this message
|
|
19
|
+
# @option kwargs [Symbol] :method The method to call on the underlying object to resolve this field (defaults to `name`)
|
|
20
|
+
# @option kwargs [String, Symbol] :hash_key The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
|
|
21
|
+
# @option kwargs [Array<String, Symbol>] :dig The nested hash keys to lookup on the underlying hash to resolve this field using dig
|
|
22
|
+
# @option kwargs [Symbol] :resolver_method The method on the type to call to resolve this field (defaults to `name`)
|
|
23
|
+
# @option kwargs [Boolean] :connection `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
|
|
24
|
+
# @option kwargs [Class] :connection_extension The extension to add, to implement connections. If `nil`, no extension is added.
|
|
25
|
+
# @option kwargs [Integer, nil] :max_page_size For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
|
|
26
|
+
# @option kwargs [Integer, nil] :default_page_size For connections, the default number of items to return from this field, or `nil` to return unlimited results.
|
|
27
|
+
# @option kwargs [Boolean] :introspection If true, this field will be marked as `#introspection?` and the name may begin with `__`
|
|
28
|
+
# @option kwargs [{String=>GraphQL::Schema::Argument, Hash}] :arguments Arguments for this field (may be added in the block, also)
|
|
29
|
+
# @option kwargs [Boolean] :camelize If true, the field name will be camelized when building the schema
|
|
30
|
+
# @option kwargs [Numeric] :complexity When provided, set the complexity for this field
|
|
31
|
+
# @option kwargs [Boolean] :scope If true, the return type's `.scope_items` method will be called on the return value
|
|
32
|
+
# @option kwargs [Symbol, String] :subscription_scope A key in `context` which will be used to scope subscription payloads
|
|
33
|
+
# @option kwargs [Array<Class, Hash<Class => Object>>] :extensions Named extensions to apply to this field (see also {#extension})
|
|
34
|
+
# @option kwargs [Hash{Class => Hash}] :directives Directives to apply to this field
|
|
35
|
+
# @option kwargs [Boolean] :trace If true, a {GraphQL::Tracing} tracer will measure this scalar field
|
|
36
|
+
# @option kwargs [Boolean] :broadcastable Whether or not this field can be distributed in subscription broadcasts
|
|
37
|
+
# @option kwargs [Language::Nodes::FieldDefinition, nil] :ast_node If this schema was parsed from definition, this AST node defined the field
|
|
38
|
+
# @option kwargs [Boolean] :method_conflict_warning If false, skip the warning if this field's method conflicts with a built-in method
|
|
39
|
+
# @option kwargs [Array<Hash>] :validates Configurations for validating this field
|
|
40
|
+
# @option kwargs [Object] :fallback_value A fallback value if the method is not defined
|
|
41
|
+
# @option kwargs [Class<GraphQL::Schema::Mutation>] :mutation
|
|
42
|
+
# @option kwargs [Class<GraphQL::Schema::Resolver>] :resolver
|
|
43
|
+
# @option kwargs [Class<GraphQL::Schema::Subscription>] :subscription
|
|
44
|
+
# @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
|
|
45
|
+
# @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
|
|
46
|
+
# @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
|
|
47
|
+
# @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
|
|
48
|
+
# @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
|
|
49
|
+
# @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
|
|
50
|
+
# @yieldparam field [GraphQL::Schema::Field] The newly-created field instance
|
|
51
|
+
# @yieldreturn [void]
|
|
10
52
|
# @return [GraphQL::Schema::Field]
|
|
11
|
-
def field(
|
|
12
|
-
|
|
53
|
+
def field(name_positional = nil, type_positional = nil, desc_positional = nil, **kwargs, &definition_block)
|
|
54
|
+
resolver = kwargs.delete(:resolver)
|
|
55
|
+
mutation = kwargs.delete(:mutation)
|
|
56
|
+
subscription = kwargs.delete(:subscription)
|
|
57
|
+
if (resolver_class = resolver || mutation || subscription)
|
|
58
|
+
# Add a reference to that parent class
|
|
59
|
+
kwargs[:resolver_class] = resolver_class
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
kwargs[:name] ||= name_positional
|
|
63
|
+
if !type_positional.nil?
|
|
64
|
+
if desc_positional
|
|
65
|
+
if kwargs[:description]
|
|
66
|
+
raise ArgumentError, "Provide description as a positional argument or `description:` keyword, but not both (#{desc_positional.inspect}, #{kwargs[:description].inspect})"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
kwargs[:description] = desc_positional
|
|
70
|
+
kwargs[:type] = type_positional
|
|
71
|
+
elsif (resolver || mutation) && type_positional.is_a?(String)
|
|
72
|
+
# The return type should be copied from the resolver, and the second positional argument is the description
|
|
73
|
+
kwargs[:description] = type_positional
|
|
74
|
+
else
|
|
75
|
+
kwargs[:type] = type_positional
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if type_positional.is_a?(Class) && type_positional < GraphQL::Schema::Mutation
|
|
79
|
+
raise ArgumentError, "Use `field #{name_positional.inspect}, mutation: Mutation, ...` to provide a mutation to this field instead"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
kwargs[:owner] = self
|
|
84
|
+
field_defn = field_class.new(**kwargs, &definition_block)
|
|
13
85
|
add_field(field_defn)
|
|
14
86
|
field_defn
|
|
15
87
|
end
|
|
@@ -232,7 +304,7 @@ module GraphQL
|
|
|
232
304
|
end
|
|
233
305
|
end
|
|
234
306
|
|
|
235
|
-
# @param [GraphQL::Schema::Field]
|
|
307
|
+
# @param field_defn [GraphQL::Schema::Field]
|
|
236
308
|
# @return [String] A warning to give when this field definition might conflict with a built-in method
|
|
237
309
|
def conflict_field_name_warning(field_defn)
|
|
238
310
|
"#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
|
|
@@ -8,6 +8,13 @@ module GraphQL
|
|
|
8
8
|
#
|
|
9
9
|
# (This is for specifying mutually exclusive sets of arguments.)
|
|
10
10
|
#
|
|
11
|
+
# If you use {GraphQL::Schema::Visibility} to hide all the arguments in a `one_of: [..]` set,
|
|
12
|
+
# then a developer-facing {GraphQL::Error} will be raised during execution. Pass `allow_all_hidden: true` to
|
|
13
|
+
# skip validation in this case instead.
|
|
14
|
+
#
|
|
15
|
+
# This validator also implements `argument ... required: :nullable`. If an argument has `required: :nullable`
|
|
16
|
+
# but it's hidden with {GraphQL::Schema::Visibility}, then this validator doesn't run.
|
|
17
|
+
#
|
|
11
18
|
# @example Require exactly one of these arguments
|
|
12
19
|
#
|
|
13
20
|
# field :update_amount, IngredientAmount, null: false do
|
|
@@ -37,15 +44,17 @@ module GraphQL
|
|
|
37
44
|
class RequiredValidator < Validator
|
|
38
45
|
# @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
|
|
39
46
|
# @param argument [Symbol] An argument that is required for this field
|
|
47
|
+
# @param allow_all_hidden [Boolean] If `true`, then this validator won't run if all the `one_of: ...` arguments have been hidden
|
|
40
48
|
# @param message [String]
|
|
41
|
-
def initialize(one_of: nil, argument: nil, message: nil, **default_options)
|
|
49
|
+
def initialize(one_of: nil, argument: nil, allow_all_hidden: nil, message: nil, **default_options)
|
|
42
50
|
@one_of = if one_of
|
|
43
51
|
one_of
|
|
44
52
|
elsif argument
|
|
45
|
-
[argument]
|
|
53
|
+
[ argument ]
|
|
46
54
|
else
|
|
47
55
|
raise ArgumentError, "`one_of:` or `argument:` must be given in `validates required: {...}`"
|
|
48
56
|
end
|
|
57
|
+
@allow_all_hidden = allow_all_hidden.nil? ? !!argument : allow_all_hidden
|
|
49
58
|
@message = message
|
|
50
59
|
super(**default_options)
|
|
51
60
|
end
|
|
@@ -54,10 +63,17 @@ module GraphQL
|
|
|
54
63
|
fully_matched_conditions = 0
|
|
55
64
|
partially_matched_conditions = 0
|
|
56
65
|
|
|
66
|
+
visible_keywords = context.types.arguments(@validated).map(&:keyword)
|
|
67
|
+
no_visible_conditions = true
|
|
68
|
+
|
|
57
69
|
if !value.nil?
|
|
58
70
|
@one_of.each do |one_of_condition|
|
|
59
71
|
case one_of_condition
|
|
60
72
|
when Symbol
|
|
73
|
+
if no_visible_conditions && visible_keywords.include?(one_of_condition)
|
|
74
|
+
no_visible_conditions = false
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
if value.key?(one_of_condition)
|
|
62
78
|
fully_matched_conditions += 1
|
|
63
79
|
end
|
|
@@ -66,6 +82,9 @@ module GraphQL
|
|
|
66
82
|
full_match = true
|
|
67
83
|
|
|
68
84
|
one_of_condition.each do |k|
|
|
85
|
+
if no_visible_conditions && visible_keywords.include?(k)
|
|
86
|
+
no_visible_conditions = false
|
|
87
|
+
end
|
|
69
88
|
if value.key?(k)
|
|
70
89
|
any_match = true
|
|
71
90
|
else
|
|
@@ -88,6 +107,18 @@ module GraphQL
|
|
|
88
107
|
end
|
|
89
108
|
end
|
|
90
109
|
|
|
110
|
+
if no_visible_conditions
|
|
111
|
+
if @allow_all_hidden
|
|
112
|
+
return nil
|
|
113
|
+
else
|
|
114
|
+
raise GraphQL::Error, <<~ERR
|
|
115
|
+
#{@validated.path} validates `required: ...` but all required arguments were hidden.
|
|
116
|
+
|
|
117
|
+
Update your schema definition to allow the client to see some fields or skip validation by adding `required: { ..., allow_all_hidden: true }`
|
|
118
|
+
ERR
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
91
122
|
if fully_matched_conditions == 1 && partially_matched_conditions == 0
|
|
92
123
|
nil # OK
|
|
93
124
|
else
|
|
@@ -10,9 +10,9 @@ module GraphQL
|
|
|
10
10
|
class Visibility
|
|
11
11
|
# @param schema [Class<GraphQL::Schema>]
|
|
12
12
|
# @param profiles [Hash<Symbol => Hash>] A hash of `name => context` pairs for preloading visibility profiles
|
|
13
|
-
# @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?`)
|
|
13
|
+
# @param preload [Boolean] if `true`, load the default schema profile and all named profiles immediately (defaults to `true` for `Rails.env.production?` and `Rails.env.staging?`)
|
|
14
14
|
# @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
|
|
15
|
-
def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? Rails.env.production? : nil), migration_errors: false)
|
|
15
|
+
def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails.env) ? (Rails.env.production? || Rails.env.staging?) : nil), migration_errors: false)
|
|
16
16
|
profiles&.each { |name, ctx|
|
|
17
17
|
ctx[:visibility_profile] = name
|
|
18
18
|
ctx.freeze
|