graphql 2.4.13 → 2.5.11

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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/query_complexity.rb +87 -7
  3. data/lib/graphql/backtrace/table.rb +37 -14
  4. data/lib/graphql/current.rb +1 -1
  5. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  6. data/lib/graphql/dashboard/installable.rb +22 -0
  7. data/lib/graphql/dashboard/limiters.rb +93 -0
  8. data/lib/graphql/dashboard/operation_store.rb +199 -0
  9. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  10. data/lib/graphql/dashboard/statics/dashboard.css +27 -0
  11. data/lib/graphql/dashboard/statics/dashboard.js +74 -9
  12. data/lib/graphql/dashboard/subscriptions.rb +96 -0
  13. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  14. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  16. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
  17. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  18. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  19. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  20. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  21. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  22. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  23. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  24. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  25. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  26. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  27. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +49 -1
  28. data/lib/graphql/dashboard.rb +45 -29
  29. data/lib/graphql/dataloader/active_record_association_source.rb +28 -8
  30. data/lib/graphql/dataloader/active_record_source.rb +26 -5
  31. data/lib/graphql/dataloader/null_dataloader.rb +7 -0
  32. data/lib/graphql/dataloader/source.rb +16 -4
  33. data/lib/graphql/dig.rb +2 -1
  34. data/lib/graphql/execution/interpreter/resolve.rb +3 -3
  35. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +34 -1
  36. data/lib/graphql/execution/interpreter/runtime.rb +163 -59
  37. data/lib/graphql/execution/interpreter.rb +5 -13
  38. data/lib/graphql/execution/multiplex.rb +6 -1
  39. data/lib/graphql/invalid_null_error.rb +15 -2
  40. data/lib/graphql/language/lexer.rb +9 -2
  41. data/lib/graphql/language/nodes.rb +5 -1
  42. data/lib/graphql/language/parser.rb +14 -6
  43. data/lib/graphql/query/context.rb +3 -8
  44. data/lib/graphql/query/partial.rb +179 -0
  45. data/lib/graphql/query.rb +59 -55
  46. data/lib/graphql/schema/addition.rb +3 -1
  47. data/lib/graphql/schema/always_visible.rb +1 -0
  48. data/lib/graphql/schema/argument.rb +9 -3
  49. data/lib/graphql/schema/build_from_definition.rb +96 -47
  50. data/lib/graphql/schema/directive/flagged.rb +2 -0
  51. data/lib/graphql/schema/directive.rb +33 -1
  52. data/lib/graphql/schema/field.rb +23 -1
  53. data/lib/graphql/schema/input_object.rb +38 -30
  54. data/lib/graphql/schema/list.rb +1 -1
  55. data/lib/graphql/schema/member/has_arguments.rb +2 -2
  56. data/lib/graphql/schema/member/has_dataloader.rb +4 -2
  57. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  58. data/lib/graphql/schema/member/has_interfaces.rb +2 -2
  59. data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
  60. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  61. data/lib/graphql/schema/resolver.rb +1 -0
  62. data/lib/graphql/schema/scalar.rb +1 -6
  63. data/lib/graphql/schema/timeout.rb +19 -2
  64. data/lib/graphql/schema/validator/required_validator.rb +15 -6
  65. data/lib/graphql/schema/visibility/migration.rb +2 -2
  66. data/lib/graphql/schema/visibility/profile.rb +107 -21
  67. data/lib/graphql/schema/visibility.rb +41 -29
  68. data/lib/graphql/schema/warden.rb +13 -5
  69. data/lib/graphql/schema.rb +228 -32
  70. data/lib/graphql/static_validation/all_rules.rb +2 -2
  71. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
  72. data/lib/graphql/static_validation/rules/fields_will_merge.rb +78 -16
  73. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
  74. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  75. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  76. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
  77. data/lib/graphql/testing/helpers.rb +5 -2
  78. data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -0
  79. data/lib/graphql/tracing/appoptics_tracing.rb +5 -0
  80. data/lib/graphql/tracing/appsignal_trace.rb +26 -61
  81. data/lib/graphql/tracing/data_dog_trace.rb +41 -164
  82. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  83. data/lib/graphql/tracing/new_relic_trace.rb +34 -164
  84. data/lib/graphql/tracing/notifications_trace.rb +183 -37
  85. data/lib/graphql/tracing/null_trace.rb +1 -1
  86. data/lib/graphql/tracing/perfetto_trace.rb +16 -19
  87. data/lib/graphql/tracing/prometheus_trace.rb +47 -74
  88. data/lib/graphql/tracing/scout_trace.rb +25 -59
  89. data/lib/graphql/tracing/sentry_trace.rb +56 -99
  90. data/lib/graphql/tracing/statsd_trace.rb +24 -47
  91. data/lib/graphql/tracing/trace.rb +0 -17
  92. data/lib/graphql/tracing.rb +1 -0
  93. data/lib/graphql/type_kinds.rb +1 -0
  94. data/lib/graphql/version.rb +1 -1
  95. data/lib/graphql.rb +1 -1
  96. metadata +35 -26
  97. data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +0 -63
  98. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
@@ -23,21 +23,22 @@ module GraphQL
23
23
  # @return [Array<GraphQL::Query::Result>] One result per query
24
24
  def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
25
25
  queries = query_options.map do |opts|
26
- case opts
26
+ query = case opts
27
27
  when Hash
28
28
  schema.query_class.new(schema, nil, **opts)
29
- when GraphQL::Query
29
+ when GraphQL::Query, GraphQL::Query::Partial
30
30
  opts
31
31
  else
32
32
  raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
33
33
  end
34
+ query
34
35
  end
35
36
 
37
+ return GraphQL::EmptyObjects::EMPTY_ARRAY if queries.empty?
36
38
 
37
39
  multiplex = Execution::Multiplex.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
38
- Fiber[:__graphql_current_multiplex] = multiplex
39
40
  trace = multiplex.current_trace
40
- trace.begin_execute_multiplex(multiplex)
41
+ Fiber[:__graphql_current_multiplex] = multiplex
41
42
  trace.execute_multiplex(multiplex: multiplex) do
42
43
  schema = multiplex.schema
43
44
  queries = multiplex.queries
@@ -92,13 +93,6 @@ module GraphQL
92
93
  # Then, work through lazy results in a breadth-first way
93
94
  multiplex.dataloader.append_job {
94
95
  query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil
95
- queries = multiplex ? multiplex.queries : [query]
96
- final_values = queries.map do |query|
97
- runtime = query.context.namespace(:interpreter_runtime)[:runtime]
98
- # it might not be present if the query has an error
99
- runtime ? runtime.final_result : nil
100
- end
101
- final_values.compact!
102
96
  multiplex.current_trace.execute_query_lazy(multiplex: multiplex, query: query) do
103
97
  Interpreter::Resolve.resolve_each_depth(lazies_at_depth, multiplex.dataloader)
104
98
  end
@@ -154,8 +148,6 @@ module GraphQL
154
148
  }
155
149
  end
156
150
  end
157
- ensure
158
- trace&.end_execute_multiplex(multiplex)
159
151
  end
160
152
  end
161
153
 
@@ -32,10 +32,15 @@ module GraphQL
32
32
  @queries = queries
33
33
  @queries.each { |q| q.multiplex = self }
34
34
  @context = context
35
- @current_trace = @context[:trace] || schema.new_trace(multiplex: self)
36
35
  @dataloader = @context[:dataloader] ||= @schema.dataloader_class.new
37
36
  @tracers = schema.tracers + (context[:tracers] || [])
38
37
  @max_complexity = max_complexity
38
+ @current_trace = context[:trace] ||= schema.new_trace(multiplex: self)
39
+ @logger = nil
40
+ end
41
+
42
+ def logger
43
+ @logger ||= @schema.logger_for(context)
39
44
  end
40
45
  end
41
46
  end
@@ -12,11 +12,24 @@ module GraphQL
12
12
  # @return [GraphQL::Language::Nodes::Field] the field where the error occurred
13
13
  attr_reader :ast_node
14
14
 
15
- def initialize(parent_type, field, ast_node)
15
+ # @return [Boolean] indicates an array result caused the error
16
+ attr_reader :is_from_array
17
+
18
+ def initialize(parent_type, field, ast_node, is_from_array: false)
16
19
  @parent_type = parent_type
17
20
  @field = field
18
21
  @ast_node = ast_node
19
- super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}")
22
+ @is_from_array = is_from_array
23
+
24
+ # For List elements, identify the non-null error is for an
25
+ # element and the required element type so it's not ambiguous
26
+ # whether it was caused by a null instead of the list or a
27
+ # null element.
28
+ if @is_from_array
29
+ super("Cannot return null for non-nullable element of type '#{@field.type.of_type.of_type.to_type_signature}' for #{@parent_type.graphql_name}.#{@field.graphql_name}")
30
+ else
31
+ super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}")
32
+ end
20
33
  end
21
34
 
22
35
  class << self
@@ -20,6 +20,11 @@ module GraphQL
20
20
  @finished
21
21
  end
22
22
 
23
+ def freeze
24
+ @scanner = nil
25
+ super
26
+ end
27
+
23
28
  attr_reader :pos, :tokens_count
24
29
 
25
30
  def advance
@@ -242,7 +247,7 @@ module GraphQL
242
247
  :SCALAR,
243
248
  nil,
244
249
  :FRAGMENT
245
- ]
250
+ ].freeze
246
251
 
247
252
  # This produces a unique integer for bytes 2 and 3 of each keyword string
248
253
  # See https://tenderlovemaking.com/2023/09/02/fast-tokenizers-with-stringscanner.html
@@ -271,7 +276,8 @@ module GraphQL
271
276
  PUNCTUATION_NAME_FOR_BYTE = Punctuation.constants.each_with_object([]) { |name, arr|
272
277
  punct = Punctuation.const_get(name)
273
278
  arr[punct.ord] = name
274
- }
279
+ }.freeze
280
+
275
281
 
276
282
  QUOTE = '"'
277
283
  UNICODE_DIGIT = /[0-9A-Za-z]/
@@ -321,6 +327,7 @@ module GraphQL
321
327
  punct = Punctuation.const_get(punct_name)
322
328
  FIRST_BYTES[punct.ord] = ByteFor::PUNCTUATION
323
329
  end
330
+ FIRST_BYTES.freeze
324
331
 
325
332
 
326
333
  # Replace any escaped unicode or whitespace with the _actual_ characters
@@ -83,7 +83,11 @@ module GraphQL
83
83
 
84
84
  def to_query_string(printer: GraphQL::Language::Printer.new)
85
85
  if printer.is_a?(GraphQL::Language::Printer)
86
- @query_string ||= printer.print(self)
86
+ if frozen?
87
+ @query_string || printer.print(self)
88
+ else
89
+ @query_string ||= printer.print(self)
90
+ end
87
91
  else
88
92
  printer.print(self)
89
93
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "strscan"
4
4
  require "graphql/language/nodes"
5
+ require "graphql/tracing/null_trace"
5
6
 
6
7
  module GraphQL
7
8
  module Language
@@ -488,26 +489,33 @@ module GraphQL
488
489
  end
489
490
 
490
491
  def type
491
- type = case token_name
492
+ parsed_type = case token_name
492
493
  when :IDENTIFIER
493
494
  parse_type_name
494
495
  when :LBRACKET
495
496
  list_type
497
+ else
498
+ nil
496
499
  end
497
500
 
498
- if at?(:BANG)
499
- type = Nodes::NonNullType.new(pos: pos, of_type: type, source: self)
501
+ if at?(:BANG) && parsed_type
502
+ parsed_type = Nodes::NonNullType.new(pos: pos, of_type: parsed_type, source: self)
500
503
  expect_token(:BANG)
501
504
  end
502
- type
505
+ parsed_type
503
506
  end
504
507
 
505
508
  def list_type
506
509
  loc = pos
507
510
  expect_token(:LBRACKET)
508
- type = Nodes::ListType.new(pos: loc, of_type: self.type, source: self)
511
+ inner_type = self.type
512
+ parsed_list_type = if inner_type
513
+ Nodes::ListType.new(pos: loc, of_type: inner_type, source: self)
514
+ else
515
+ nil
516
+ end
509
517
  expect_token(:RBRACKET)
510
- type
518
+ parsed_list_type
511
519
  end
512
520
 
513
521
  def parse_operation_type
@@ -39,9 +39,6 @@ module GraphQL
39
39
  # @return [GraphQL::Schema]
40
40
  attr_reader :schema
41
41
 
42
- # @return [Array<String, Integer>] The current position in the result
43
- attr_reader :path
44
-
45
42
  # Make a new context which delegates key lookup to `values`
46
43
  # @param query [GraphQL::Query] the query who owns this context
47
44
  # @param values [Hash] A hash of arbitrary values which will be accessible at query-time
@@ -53,12 +50,10 @@ module GraphQL
53
50
  @storage = Hash.new { |h, k| h[k] = {} }
54
51
  @storage[nil] = @provided_values
55
52
  @errors = []
56
- @path = []
57
- @value = nil
58
- @context = self # for SharedMethods TODO delete sharedmethods
59
53
  @scoped_context = ScopedContext.new(self)
60
54
  end
61
55
 
56
+ # Modify this hash to return extensions to client.
62
57
  # @return [Hash] A hash that will be added verbatim to the result hash, as `"extensions" => { ... }`
63
58
  def response_extensions
64
59
  namespace(:__query_result_extensions__)
@@ -89,7 +84,7 @@ module GraphQL
89
84
 
90
85
  attr_writer :types
91
86
 
92
- RUNTIME_METADATA_KEYS = Set.new([:current_object, :current_arguments, :current_field, :current_path])
87
+ RUNTIME_METADATA_KEYS = Set.new([:current_object, :current_arguments, :current_field, :current_path]).freeze
93
88
  # @!method []=(key, value)
94
89
  # Reassign `key` to the hash passed to {Schema#execute} as `context:`
95
90
 
@@ -244,7 +239,7 @@ module GraphQL
244
239
  end
245
240
 
246
241
  def inspect
247
- "#<Query::Context ...>"
242
+ "#<#{self.class} ...>"
248
243
  end
249
244
 
250
245
  def scoped_merge!(hash)
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Query
4
+ # This class is _like_ a {GraphQL::Query}, except it can run on an arbitrary path within a query string.
5
+ #
6
+ # It depends on a "parent" {Query}.
7
+ #
8
+ # During execution, it calls query-related tracing hooks but passes itself as `query:`.
9
+ #
10
+ # The {Partial} will use your {Schema.resolve_type} hook to find the right GraphQL type to use for
11
+ # `object` in some cases.
12
+ #
13
+ # @see Query#run_partials Run via {Query#run_partials}
14
+ class Partial
15
+ include Query::Runnable
16
+
17
+ # @param path [Array<String, Integer>] A path in `query.query_string` to start executing from
18
+ # @param object [Object] A starting object for execution
19
+ # @param query [GraphQL::Query] A full query instance that this partial is based on. Caches are shared.
20
+ # @param context [Hash] Extra context values to merge into `query.context`, if provided
21
+ # @param fragment_node [GraphQL::Language::Nodes::InlineFragment, GraphQL::Language::Nodes::FragmentDefinition]
22
+ def initialize(path: nil, object:, query:, context: nil, fragment_node: nil, type: nil)
23
+ @path = path
24
+ @object = object
25
+ @query = query
26
+ @schema = query.schema
27
+ context_vals = @query.context.to_h
28
+ if context
29
+ context_vals = context_vals.merge(context)
30
+ end
31
+ @context = GraphQL::Query::Context.new(query: self, schema: @query.schema, values: context_vals)
32
+ @multiplex = nil
33
+ @result_values = nil
34
+ @result = nil
35
+
36
+ if fragment_node
37
+ @ast_nodes = [fragment_node]
38
+ @root_type = type || raise(ArgumentError, "Pass `type:` when using `node:`")
39
+ # This is only used when `@leaf`
40
+ @field_definition = nil
41
+ elsif path.nil?
42
+ raise ArgumentError, "`path:` is required if `node:` is not given; add `path:`"
43
+ else
44
+ set_type_info_from_path
45
+ end
46
+
47
+ @leaf = @root_type.unwrap.kind.leaf?
48
+ end
49
+
50
+ def leaf?
51
+ @leaf
52
+ end
53
+
54
+ attr_reader :context, :query, :ast_nodes, :root_type, :object, :field_definition, :path, :schema
55
+
56
+ attr_accessor :multiplex, :result_values
57
+
58
+ class Result < GraphQL::Query::Result
59
+ def path
60
+ @query.path
61
+ end
62
+
63
+ # @return [GraphQL::Query::Partial]
64
+ def partial
65
+ @query
66
+ end
67
+ end
68
+
69
+ def result
70
+ @result ||= Result.new(query: self, values: result_values)
71
+ end
72
+
73
+ def current_trace
74
+ @query.current_trace
75
+ end
76
+
77
+ def types
78
+ @query.types
79
+ end
80
+
81
+ def resolve_type(...)
82
+ @query.resolve_type(...)
83
+ end
84
+
85
+ def variables
86
+ @query.variables
87
+ end
88
+
89
+ def fragments
90
+ @query.fragments
91
+ end
92
+
93
+ def valid?
94
+ @query.valid?
95
+ end
96
+
97
+ def analyzers
98
+ EmptyObjects::EMPTY_ARRAY
99
+ end
100
+
101
+ def analysis_errors=(_ignored)
102
+ # pass
103
+ end
104
+
105
+ def subscription?
106
+ @query.subscription?
107
+ end
108
+
109
+ def selected_operation
110
+ ast_nodes.first
111
+ end
112
+
113
+ def static_errors
114
+ @query.static_errors
115
+ end
116
+
117
+ def selected_operation_name
118
+ @query.selected_operation_name
119
+ end
120
+
121
+ private
122
+
123
+ def set_type_info_from_path
124
+ selections = [@query.selected_operation]
125
+ type = @query.root_type
126
+ parent_type = nil
127
+ field_defn = nil
128
+
129
+ @path.each do |name_in_doc|
130
+ if name_in_doc.is_a?(Integer)
131
+ if type.list?
132
+ type = type.unwrap
133
+ next
134
+ else
135
+ raise ArgumentError, "Received path with index `#{name_in_doc}`, but type wasn't a list. Type: #{type.to_type_signature}, path: #{@path}"
136
+ end
137
+ end
138
+
139
+ next_selections = []
140
+ selections.each do |selection|
141
+ selections_to_check = []
142
+ selections_to_check.concat(selection.selections)
143
+ while (sel = selections_to_check.shift)
144
+ case sel
145
+ when GraphQL::Language::Nodes::InlineFragment
146
+ selections_to_check.concat(sel.selections)
147
+ when GraphQL::Language::Nodes::FragmentSpread
148
+ fragment = @query.fragments[sel.name]
149
+ selections_to_check.concat(fragment.selections)
150
+ when GraphQL::Language::Nodes::Field
151
+ if sel.alias == name_in_doc || sel.name == name_in_doc
152
+ next_selections << sel
153
+ end
154
+ else
155
+ raise "Unexpected selection in partial path: #{sel.class}, #{sel.inspect}"
156
+ end
157
+ end
158
+ end
159
+
160
+ if next_selections.empty?
161
+ raise ArgumentError, "Path `#{@path.inspect}` is not present in this query. `#{name_in_doc.inspect}` was not found. Try a different path or rewrite the query to include it."
162
+ end
163
+ field_name = next_selections.first.name
164
+ field_defn = @schema.get_field(type, field_name, @query.context) || raise("Invariant: no field called #{field_name} on #{type.graphql_name}")
165
+ parent_type = type
166
+ type = field_defn.type
167
+ if type.non_null?
168
+ type = type.of_type
169
+ end
170
+ selections = next_selections
171
+ end
172
+
173
+ @ast_nodes = selections
174
+ @root_type = type
175
+ @field_definition = field_defn
176
+ end
177
+ end
178
+ end
179
+ end
data/lib/graphql/query.rb CHANGED
@@ -10,12 +10,47 @@ module GraphQL
10
10
  autoload :Context, "graphql/query/context"
11
11
  autoload :Fingerprint, "graphql/query/fingerprint"
12
12
  autoload :NullContext, "graphql/query/null_context"
13
+ autoload :Partial, "graphql/query/partial"
13
14
  autoload :Result, "graphql/query/result"
14
15
  autoload :Variables, "graphql/query/variables"
15
16
  autoload :InputValidationResult, "graphql/query/input_validation_result"
16
17
  autoload :VariableValidationError, "graphql/query/variable_validation_error"
17
18
  autoload :ValidationPipeline, "graphql/query/validation_pipeline"
18
19
 
20
+ # Code shared with {Partial}
21
+ module Runnable
22
+ def after_lazy(value, &block)
23
+ if !defined?(@runtime_instance)
24
+ @runtime_instance = context.namespace(:interpreter_runtime)[:runtime]
25
+ end
26
+
27
+ if @runtime_instance
28
+ @runtime_instance.minimal_after_lazy(value, &block)
29
+ else
30
+ @schema.after_lazy(value, &block)
31
+ end
32
+ end
33
+
34
+ # Node-level cache for calculating arguments. Used during execution and query analysis.
35
+ # @param ast_node [GraphQL::Language::Nodes::AbstractNode]
36
+ # @param definition [GraphQL::Schema::Field]
37
+ # @param parent_object [GraphQL::Schema::Object]
38
+ # @return [Hash{Symbol => Object}]
39
+ def arguments_for(ast_node, definition, parent_object: nil)
40
+ arguments_cache.fetch(ast_node, definition, parent_object)
41
+ end
42
+
43
+ def arguments_cache
44
+ @arguments_cache ||= Execution::Interpreter::ArgumentsCache.new(self)
45
+ end
46
+
47
+ # @api private
48
+ def handle_or_reraise(err)
49
+ @schema.handle_or_reraise(context, err)
50
+ end
51
+ end
52
+
53
+ include Runnable
19
54
  class OperationNameMissingError < GraphQL::ExecutionError
20
55
  def initialize(name)
21
56
  msg = if name.nil?
@@ -50,7 +85,7 @@ module GraphQL
50
85
  # @return [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules.
51
86
  attr_reader :static_validator
52
87
 
53
- # @param new_validate [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules. This can't be reasssigned after validation.
88
+ # @param new_validator [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules. This can't be reasssigned after validation.
54
89
  def static_validator=(new_validator)
55
90
  if defined?(@validation_pipeline) && @validation_pipeline && @validation_pipeline.has_validated?
56
91
  raise ArgumentError, "Can't reassign Query#static_validator= after validation has run, remove this assignment."
@@ -98,9 +133,10 @@ module GraphQL
98
133
  # @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value)
99
134
  # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
100
135
  # @param visibility_profile [Symbol] Another way to assign `context[:visibility_profile]`
101
- def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, validate: true, static_validator: nil, visibility_profile: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil, use_visibility_profile: nil)
136
+ def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, multiplex: nil, validate: true, static_validator: nil, visibility_profile: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil, use_visibility_profile: nil)
102
137
  # Even if `variables: nil` is passed, use an empty hash for simpler logic
103
138
  variables ||= {}
139
+ @multiplex = multiplex
104
140
  @schema = schema
105
141
  @context = schema.context_class.new(query: self, values: context)
106
142
  if visibility_profile
@@ -171,13 +207,7 @@ module GraphQL
171
207
  @result_values = nil
172
208
  @executed = false
173
209
 
174
- @logger = if context && context[:logger] == false
175
- Logger.new(IO::NULL)
176
- elsif context && (l = context[:logger])
177
- l
178
- else
179
- schema.default_logger
180
- end
210
+ @logger = schema.logger_for(context)
181
211
  end
182
212
 
183
213
  # If a document was provided to `GraphQL::Schema#execute` instead of the raw query string, we will need to get it from the document
@@ -203,19 +233,10 @@ module GraphQL
203
233
  # @return [GraphQL::Execution::Lookahead]
204
234
  def lookahead
205
235
  @lookahead ||= begin
206
- ast_node = selected_operation
207
- if ast_node.nil?
236
+ if selected_operation.nil?
208
237
  GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
209
238
  else
210
- root_type = case ast_node.operation_type
211
- when nil, "query"
212
- types.query_root # rubocop:disable Development/ContextIsPassedCop
213
- when "mutation"
214
- types.mutation_root # rubocop:disable Development/ContextIsPassedCop
215
- when "subscription"
216
- types.subscription_root # rubocop:disable Development/ContextIsPassedCop
217
- end
218
- GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node])
239
+ GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [selected_operation])
219
240
  end
220
241
  end
221
242
  end
@@ -241,6 +262,18 @@ module GraphQL
241
262
  with_prepared_ast { @operations }
242
263
  end
243
264
 
265
+ # Run subtree partials of this query and return their results.
266
+ # Each partial is identified with a `path:` and `object:`
267
+ # where the path references a field in the AST and the object will be treated
268
+ # as the return value from that field. Subfields of the field named by `path`
269
+ # will be executed with `object` as the starting point
270
+ # @param partials_hashes [Array<Hash{Symbol => Object}>] Hashes with `path:` and `object:` keys
271
+ # @return [Array<GraphQL::Query::Result>]
272
+ def run_partials(partials_hashes)
273
+ partials = partials_hashes.map { |partial_options| Partial.new(query: self, **partial_options) }
274
+ Execution::Interpreter.run_all(@schema, partials, context: @context)
275
+ end
276
+
244
277
  # Get the result for this query, executing it once
245
278
  # @return [GraphQL::Query::Result] A Hash-like GraphQL response, with `"data"` and/or `"errors"` keys
246
279
  def result
@@ -283,19 +316,6 @@ module GraphQL
283
316
  end
284
317
  end
285
318
 
286
- # Node-level cache for calculating arguments. Used during execution and query analysis.
287
- # @param ast_node [GraphQL::Language::Nodes::AbstractNode]
288
- # @param definition [GraphQL::Schema::Field]
289
- # @param parent_object [GraphQL::Schema::Object]
290
- # @return Hash{Symbol => Object}
291
- def arguments_for(ast_node, definition, parent_object: nil)
292
- arguments_cache.fetch(ast_node, definition, parent_object)
293
- end
294
-
295
- def arguments_cache
296
- @arguments_cache ||= Execution::Interpreter::ArgumentsCache.new(self)
297
- end
298
-
299
319
  # A version of the given query string, with:
300
320
  # - Variables inlined to the query
301
321
  # - Strings replaced with `<REDACTED>`
@@ -362,17 +382,21 @@ module GraphQL
362
382
 
363
383
  def root_type_for_operation(op_type)
364
384
  case op_type
365
- when "query"
385
+ when "query", nil
366
386
  types.query_root # rubocop:disable Development/ContextIsPassedCop
367
387
  when "mutation"
368
388
  types.mutation_root # rubocop:disable Development/ContextIsPassedCop
369
389
  when "subscription"
370
390
  types.subscription_root # rubocop:disable Development/ContextIsPassedCop
371
391
  else
372
- raise ArgumentError, "unexpected root type name: #{op_type.inspect}; expected 'query', 'mutation', or 'subscription'"
392
+ raise ArgumentError, "unexpected root type name: #{op_type.inspect}; expected nil, 'query', 'mutation', or 'subscription'"
373
393
  end
374
394
  end
375
395
 
396
+ def root_type
397
+ root_type_for_operation(selected_operation.operation_type)
398
+ end
399
+
376
400
  def types
377
401
  @visibility_profile || warden.visibility_profile
378
402
  end
@@ -405,23 +429,6 @@ module GraphQL
405
429
  with_prepared_ast { @subscription }
406
430
  end
407
431
 
408
- # @api private
409
- def handle_or_reraise(err)
410
- schema.handle_or_reraise(context, err)
411
- end
412
-
413
- def after_lazy(value, &block)
414
- if !defined?(@runtime_instance)
415
- @runtime_instance = context.namespace(:interpreter_runtime)[:runtime]
416
- end
417
-
418
- if @runtime_instance
419
- @runtime_instance.minimal_after_lazy(value, &block)
420
- else
421
- @schema.after_lazy(value, &block)
422
- end
423
- end
424
-
425
432
  attr_reader :logger
426
433
 
427
434
  private
@@ -441,7 +448,6 @@ module GraphQL
441
448
  @warden ||= @schema.warden_class.new(schema: @schema, context: @context)
442
449
  parse_error = nil
443
450
  @document ||= begin
444
- current_trace.begin_parse(query_string)
445
451
  if query_string
446
452
  GraphQL.parse(query_string, trace: self.current_trace, max_tokens: @schema.max_query_string_tokens)
447
453
  end
@@ -449,8 +455,6 @@ module GraphQL
449
455
  parse_error = err
450
456
  @schema.parse_error(err, @context)
451
457
  nil
452
- ensure
453
- current_trace.end_parse(query_string)
454
458
  end
455
459
 
456
460
  @fragments = {}
@@ -258,7 +258,9 @@ module GraphQL
258
258
  # We can get these now; we'll have to get late-bound types later
259
259
  if interface_type.is_a?(Module) && type.is_a?(Class)
260
260
  implementers = @possible_types[interface_type] ||= []
261
- implementers << type
261
+ if !implementers.include?(type)
262
+ implementers << type
263
+ end
262
264
  end
263
265
  when String, Schema::LateBoundType
264
266
  interface_type = interface_type_membership
@@ -3,6 +3,7 @@ module GraphQL
3
3
  class Schema
4
4
  module AlwaysVisible
5
5
  def self.use(schema, **opts)
6
+ schema.use(GraphQL::Schema::Visibility, profiles: { nil => {} })
6
7
  schema.extend(self)
7
8
  end
8
9
 
@@ -212,12 +212,17 @@ module GraphQL
212
212
  @statically_coercible = !requires_parent_object
213
213
  end
214
214
 
215
+ def freeze
216
+ statically_coercible?
217
+ super
218
+ end
219
+
215
220
  # Apply the {prepare} configuration to `value`, using methods from `obj`.
216
221
  # Used by the runtime.
217
222
  # @api private
218
223
  def prepare_value(obj, value, context: nil)
219
224
  if type.unwrap.kind.input_object?
220
- value = recursively_prepare_input_object(value, type)
225
+ value = recursively_prepare_input_object(value, type, context)
221
226
  end
222
227
 
223
228
  Schema::Validator.validate!(validators, obj, context, value)
@@ -398,15 +403,16 @@ module GraphQL
398
403
 
399
404
  private
400
405
 
401
- def recursively_prepare_input_object(value, type)
406
+ def recursively_prepare_input_object(value, type, context)
402
407
  if type.non_null?
403
408
  type = type.of_type
404
409
  end
405
410
 
406
411
  if type.list? && !value.nil?
407
412
  inner_type = type.of_type
408
- value.map { |v| recursively_prepare_input_object(v, inner_type) }
413
+ value.map { |v| recursively_prepare_input_object(v, inner_type, context) }
409
414
  elsif value.is_a?(GraphQL::Schema::InputObject)
415
+ value.validate_for(context)
410
416
  value.prepare
411
417
  else
412
418
  value