graphql 2.3.18 → 2.4.1

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/dataloader/async_dataloader.rb +3 -2
  3. data/lib/graphql/dataloader/source.rb +1 -1
  4. data/lib/graphql/dataloader.rb +31 -10
  5. data/lib/graphql/query/null_context.rb +3 -5
  6. data/lib/graphql/query.rb +49 -16
  7. data/lib/graphql/schema/always_visible.rb +6 -3
  8. data/lib/graphql/schema/argument.rb +1 -0
  9. data/lib/graphql/schema/build_from_definition.rb +1 -0
  10. data/lib/graphql/schema/enum.rb +19 -3
  11. data/lib/graphql/schema/enum_value.rb +1 -1
  12. data/lib/graphql/schema/input_object.rb +20 -7
  13. data/lib/graphql/schema/member/has_arguments.rb +2 -2
  14. data/lib/graphql/schema/member/has_fields.rb +2 -2
  15. data/lib/graphql/schema/printer.rb +1 -0
  16. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  17. data/lib/graphql/schema/visibility/migration.rb +34 -35
  18. data/lib/graphql/schema/visibility/{subset.rb → profile.rb} +37 -19
  19. data/lib/graphql/schema/visibility.rb +57 -12
  20. data/lib/graphql/schema/warden.rb +87 -21
  21. data/lib/graphql/schema.rb +177 -41
  22. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  23. data/lib/graphql/static_validation/rules/directives_are_defined.rb +2 -1
  24. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +2 -1
  25. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -0
  26. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +11 -1
  27. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +10 -1
  28. data/lib/graphql/static_validation/validation_context.rb +15 -0
  29. data/lib/graphql/testing/helpers.rb +1 -1
  30. data/lib/graphql/version.rb +1 -1
  31. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e0bed192e241186842ca39b44c83c48f671afc972369ae621e99a35207c8a28
4
- data.tar.gz: ae7547ce51cda8dbb8ae800248dd33baa85807fe08bfdeff2ca438ee3e00634a
3
+ metadata.gz: 12a25d94ae9348527390bad889be7c918390c0a1b133735612ac5a80ca0c6633
4
+ data.tar.gz: de4bb61a31dc643d359157dff498410f70a5a7bd18d6c254463a2a5294706949
5
5
  SHA512:
6
- metadata.gz: dc98ab1793f2331d09f2f768e39fe25de559de7c541d7c6eb1231d2bd81191b5aae965136a496abaeeeaa0306d734fff39a18f802aca9ee1acdfa7a2f48078b9
7
- data.tar.gz: f27d94dc58836926fed62c5102f264fd4c473ded461888c22f3717da535a33751cfcfbd4a6d060fd3093799368fa88833fbf694b55c396d53a580797dc80800f
6
+ metadata.gz: f85c5a5e4ab0083862c990fe6cd3f0ed99d36a414721d4c3d0a7a3e5c59042cdc5611a6b74d10d0e18525d9e51a32dc6aa1a2349c789b04dae75a835f4bb322a
7
+ data.tar.gz: e0304dded75753771417672c704054a1daec13ab428ed92f97dbdcee2771f21874bfb6aedf9bd0054806ed97b4483b98376f03526b129d96628735c1933f0d35
@@ -12,6 +12,7 @@ module GraphQL
12
12
  end
13
13
 
14
14
  def run
15
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
15
16
  job_fibers = []
16
17
  next_job_fibers = []
17
18
  source_tasks = []
@@ -23,7 +24,7 @@ module GraphQL
23
24
  first_pass = false
24
25
  fiber_vars = get_fiber_variables
25
26
 
26
- while (f = (job_fibers.shift || spawn_job_fiber))
27
+ while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber)))
27
28
  if f.alive?
28
29
  finished = run_fiber(f)
29
30
  if !finished
@@ -37,7 +38,7 @@ module GraphQL
37
38
  Sync do |root_task|
38
39
  set_fiber_variables(fiber_vars)
39
40
  while source_tasks.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
40
- while (task = source_tasks.shift || spawn_source_task(root_task, sources_condition))
41
+ while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition))))
41
42
  if task.alive?
42
43
  root_task.yield # give the source task a chance to run
43
44
  next_source_tasks << task
@@ -98,7 +98,7 @@ module GraphQL
98
98
  while pending_result_keys.any? { |key| !@results.key?(key) }
99
99
  iterations += 1
100
100
  if iterations > MAX_ITERATIONS
101
- raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency."
101
+ raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency#{@dataloader.fiber_limit ? " or `fiber_limit: #{@dataloader.fiber_limit}` is set too low" : ""}."
102
102
  end
103
103
  @dataloader.yield
104
104
  end
@@ -24,18 +24,23 @@ module GraphQL
24
24
  #
25
25
  class Dataloader
26
26
  class << self
27
- attr_accessor :default_nonblocking
27
+ attr_accessor :default_nonblocking, :default_fiber_limit
28
28
  end
29
29
 
30
- NonblockingDataloader = Class.new(self) { self.default_nonblocking = true }
31
-
32
- def self.use(schema, nonblocking: nil)
33
- schema.dataloader_class = if nonblocking
30
+ def self.use(schema, nonblocking: nil, fiber_limit: nil)
31
+ dataloader_class = if nonblocking
34
32
  warn("`nonblocking: true` is deprecated from `GraphQL::Dataloader`, please use `GraphQL::Dataloader::AsyncDataloader` instead. Docs: https://graphql-ruby.org/dataloader/async_dataloader.")
35
- NonblockingDataloader
33
+ Class.new(self) { self.default_nonblocking = true }
36
34
  else
37
35
  self
38
36
  end
37
+
38
+ if fiber_limit
39
+ dataloader_class = Class.new(dataloader_class)
40
+ dataloader_class.default_fiber_limit = fiber_limit
41
+ end
42
+
43
+ schema.dataloader_class = dataloader_class
39
44
  end
40
45
 
41
46
  # Call the block with a Dataloader instance,
@@ -50,14 +55,18 @@ module GraphQL
50
55
  result
51
56
  end
52
57
 
53
- def initialize(nonblocking: self.class.default_nonblocking)
58
+ def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit)
54
59
  @source_cache = Hash.new { |h, k| h[k] = {} }
55
60
  @pending_jobs = []
56
61
  if !nonblocking.nil?
57
62
  @nonblocking = nonblocking
58
63
  end
64
+ @fiber_limit = fiber_limit
59
65
  end
60
66
 
67
+ # @return [Integer, nil]
68
+ attr_reader :fiber_limit
69
+
61
70
  def nonblocking?
62
71
  @nonblocking
63
72
  end
@@ -178,6 +187,7 @@ module GraphQL
178
187
  end
179
188
 
180
189
  def run
190
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
181
191
  job_fibers = []
182
192
  next_job_fibers = []
183
193
  source_fibers = []
@@ -187,7 +197,7 @@ module GraphQL
187
197
  while first_pass || job_fibers.any?
188
198
  first_pass = false
189
199
 
190
- while (f = (job_fibers.shift || spawn_job_fiber))
200
+ while (f = (job_fibers.shift || (((next_job_fibers.size + job_fibers.size) < jobs_fiber_limit) && spawn_job_fiber)))
191
201
  if f.alive?
192
202
  finished = run_fiber(f)
193
203
  if !finished
@@ -197,8 +207,8 @@ module GraphQL
197
207
  end
198
208
  join_queues(job_fibers, next_job_fibers)
199
209
 
200
- while source_fibers.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
201
- while (f = source_fibers.shift || spawn_source_fiber)
210
+ while (source_fibers.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) })
211
+ while (f = source_fibers.shift || (((job_fibers.size + source_fibers.size + next_source_fibers.size + next_job_fibers.size) < total_fiber_limit) && spawn_source_fiber))
202
212
  if f.alive?
203
213
  finished = run_fiber(f)
204
214
  if !finished
@@ -242,6 +252,17 @@ module GraphQL
242
252
 
243
253
  private
244
254
 
255
+ def calculate_fiber_limit
256
+ total_fiber_limit = @fiber_limit || Float::INFINITY
257
+ if total_fiber_limit < 4
258
+ raise ArgumentError, "Dataloader fiber limit is too low (#{total_fiber_limit}), it must be at least 4"
259
+ end
260
+ total_fiber_limit -= 1 # deduct one fiber for `manager`
261
+ # Deduct at least one fiber for sources
262
+ jobs_fiber_limit = total_fiber_limit - 2
263
+ return jobs_fiber_limit, total_fiber_limit
264
+ end
265
+
245
266
  def join_queues(prev_queue, new_queue)
246
267
  @nonblocking && Fiber.scheduler.run
247
268
  prev_queue.concat(new_queue)
@@ -18,17 +18,15 @@ module GraphQL
18
18
  extend Forwardable
19
19
 
20
20
  attr_reader :schema, :query, :warden, :dataloader
21
- def_delegators GraphQL::EmptyObjects::EMPTY_HASH, :[], :fetch, :dig, :key?
21
+ def_delegators GraphQL::EmptyObjects::EMPTY_HASH, :[], :fetch, :dig, :key?, :to_h
22
22
 
23
23
  def initialize
24
24
  @query = NullQuery.new
25
25
  @dataloader = GraphQL::Dataloader::NullDataloader.new
26
26
  @schema = NullSchema
27
27
  @warden = Schema::Warden::NullWarden.new(context: self, schema: @schema)
28
- end
29
-
30
- def types
31
- @types ||= GraphQL::Schema::Warden::SchemaSubset.new(@warden)
28
+ @types = @warden.visibility_profile
29
+ freeze
32
30
  end
33
31
  end
34
32
  end
data/lib/graphql/query.rb CHANGED
@@ -95,21 +95,24 @@ module GraphQL
95
95
  # @param root_value [Object] the object used to resolve fields on the root type
96
96
  # @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value)
97
97
  # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
98
- def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, validate: true, static_validator: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil, use_schema_subset: nil)
98
+ # @param visibility_profile [Symbol]
99
+ 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)
99
100
  # Even if `variables: nil` is passed, use an empty hash for simpler logic
100
101
  variables ||= {}
101
102
  @schema = schema
102
103
  @context = schema.context_class.new(query: self, values: context)
103
104
 
104
- if use_schema_subset.nil?
105
- use_schema_subset = warden ? false : schema.use_schema_visibility?
105
+ if use_visibility_profile.nil?
106
+ use_visibility_profile = warden ? false : schema.use_visibility_profile?
106
107
  end
107
108
 
108
- if use_schema_subset
109
- @schema_subset = @schema.subset_class.new(context: @context, schema: @schema)
109
+ @visibility_profile = visibility_profile
110
+
111
+ if use_visibility_profile
112
+ @visibility_profile = @schema.visibility.profile_for(@context, visibility_profile)
110
113
  @warden = Schema::Warden::NullWarden.new(context: @context, schema: @schema)
111
114
  else
112
- @schema_subset = nil
115
+ @visibility_profile = nil
113
116
  @warden = warden
114
117
  end
115
118
 
@@ -187,6 +190,9 @@ module GraphQL
187
190
  @query_string ||= (document ? document.to_query_string : nil)
188
191
  end
189
192
 
193
+ # @return [Symbol, nil]
194
+ attr_reader :visibility_profile
195
+
190
196
  attr_accessor :multiplex
191
197
 
192
198
  # @return [GraphQL::Tracing::Trace]
@@ -203,15 +209,19 @@ module GraphQL
203
209
  def lookahead
204
210
  @lookahead ||= begin
205
211
  ast_node = selected_operation
206
- root_type = case ast_node.operation_type
207
- when nil, "query"
208
- types.query_root # rubocop:disable Development/ContextIsPassedCop
209
- when "mutation"
210
- types.mutation_root # rubocop:disable Development/ContextIsPassedCop
211
- when "subscription"
212
- types.subscription_root # rubocop:disable Development/ContextIsPassedCop
212
+ if ast_node.nil?
213
+ GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
214
+ else
215
+ root_type = case ast_node.operation_type
216
+ when nil, "query"
217
+ types.query_root # rubocop:disable Development/ContextIsPassedCop
218
+ when "mutation"
219
+ types.mutation_root # rubocop:disable Development/ContextIsPassedCop
220
+ when "subscription"
221
+ types.subscription_root # rubocop:disable Development/ContextIsPassedCop
222
+ end
223
+ GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node])
213
224
  end
214
- GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node])
215
225
  end
216
226
  end
217
227
 
@@ -343,10 +353,33 @@ module GraphQL
343
353
  with_prepared_ast { @warden }
344
354
  end
345
355
 
346
- def_delegators :warden, :get_type, :get_field, :possible_types, :root_type_for_operation
356
+ def get_type(type_name)
357
+ types.type(type_name) # rubocop:disable Development/ContextIsPassedCop
358
+ end
359
+
360
+ def get_field(owner, field_name)
361
+ types.field(owner, field_name) # rubocop:disable Development/ContextIsPassedCop
362
+ end
363
+
364
+ def possible_types(type)
365
+ types.possible_types(type) # rubocop:disable Development/ContextIsPassedCop
366
+ end
367
+
368
+ def root_type_for_operation(op_type)
369
+ case op_type
370
+ when "query"
371
+ types.query_root # rubocop:disable Development/ContextIsPassedCop
372
+ when "mutation"
373
+ types.mutation_root # rubocop:disable Development/ContextIsPassedCop
374
+ when "subscription"
375
+ types.subscription_root # rubocop:disable Development/ContextIsPassedCop
376
+ else
377
+ raise ArgumentError, "unexpected root type name: #{op_type.inspect}; expected 'query', 'mutation', or 'subscription'"
378
+ end
379
+ end
347
380
 
348
381
  def types
349
- @schema_subset || warden.schema_subset
382
+ @visibility_profile || warden.visibility_profile
350
383
  end
351
384
 
352
385
  # @param abstract_type [GraphQL::UnionType, GraphQL::InterfaceType]
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
3
  class Schema
4
- class AlwaysVisible
4
+ module AlwaysVisible
5
5
  def self.use(schema, **opts)
6
- schema.warden_class = GraphQL::Schema::Warden::NullWarden
7
- schema.subset_class = GraphQL::Schema::Warden::NullWarden::NullSubset
6
+ schema.extend(self)
7
+ end
8
+
9
+ def visible?(_member, _context)
10
+ true
8
11
  end
9
12
  end
10
13
  end
@@ -364,6 +364,7 @@ module GraphQL
364
364
 
365
365
  # @api private
366
366
  def validate_default_value
367
+ return unless default_value?
367
368
  coerced_default_value = begin
368
369
  # This is weird, but we should accept single-item default values for list-type arguments.
369
370
  # If we used `coerce_isolated_input` below, it would do this for us, but it's not really
@@ -188,6 +188,7 @@ module GraphQL
188
188
 
189
189
  def self.inherited(child_class)
190
190
  child_class.definition_default_resolve = self.definition_default_resolve
191
+ super
191
192
  end
192
193
  end
193
194
 
@@ -86,10 +86,24 @@ module GraphQL
86
86
  def enum_values(context = GraphQL::Query::NullContext.instance)
87
87
  inherited_values = superclass.respond_to?(:enum_values) ? superclass.enum_values(context) : nil
88
88
  visible_values = []
89
- warden = Warden.from_context(context)
89
+ types = Warden.types_from_context(context)
90
90
  own_values.each do |key, values_entry|
91
- if (v = Warden.visible_entry?(:visible_enum_value?, values_entry, context, warden))
92
- visible_values << v
91
+ visible_value = nil
92
+ if values_entry.is_a?(Array)
93
+ values_entry.each do |v|
94
+ if types.visible_enum_value?(v, context)
95
+ if visible_value.nil?
96
+ visible_value = v
97
+ visible_values << v
98
+ else
99
+ raise DuplicateNamesError.new(
100
+ duplicated_name: v.path, duplicated_definition_1: visible_value.inspect, duplicated_definition_2: v.inspect
101
+ )
102
+ end
103
+ end
104
+ end
105
+ elsif types.visible_enum_value?(values_entry, context)
106
+ visible_values << values_entry
93
107
  end
94
108
  end
95
109
 
@@ -153,6 +167,8 @@ module GraphQL
153
167
  else
154
168
  nil
155
169
  end
170
+ # rescue MissingValuesError
171
+ # nil
156
172
  end
157
173
 
158
174
  # Called by the runtime when a field returns a value to give back to the client.
@@ -74,7 +74,7 @@ module GraphQL
74
74
  end
75
75
 
76
76
  def inspect
77
- "#<#{self.class} #{path} @value=#{@value.inspect}#{description ? " @description=#{description.inspect}" : ""}>"
77
+ "#<#{self.class} #{path} @value=#{@value.inspect}#{description ? " @description=#{description.inspect}" : ""}#{deprecation_reason ? " @deprecation_reason=#{deprecation_reason.inspect}" : ""}>"
78
78
  end
79
79
 
80
80
  def visible?(_ctx); true; end
@@ -133,12 +133,14 @@ module GraphQL
133
133
  end
134
134
  # Add a method access
135
135
  method_name = argument_defn.keyword
136
- class_eval <<-RUBY, __FILE__, __LINE__
137
- def #{method_name}
138
- self[#{method_name.inspect}]
139
- end
140
- alias_method :#{method_name}, :#{method_name}
141
- RUBY
136
+ suppress_redefinition_warning do
137
+ class_eval <<-RUBY, __FILE__, __LINE__
138
+ def #{method_name}
139
+ self[#{method_name.inspect}]
140
+ end
141
+ alias_method :#{method_name}, :#{method_name}
142
+ RUBY
143
+ end
142
144
  argument_defn
143
145
  end
144
146
 
@@ -163,7 +165,7 @@ module GraphQL
163
165
 
164
166
  # Inject missing required arguments
165
167
  missing_required_inputs = ctx.types.arguments(self).reduce({}) do |m, (argument)|
166
- if !input.key?(argument.graphql_name) && argument.type.non_null? && types.argument(self, argument.graphql_name)
168
+ if !input.key?(argument.graphql_name) && argument.type.non_null? && !argument.default_value? && types.argument(self, argument.graphql_name)
167
169
  m[argument.graphql_name] = nil
168
170
  end
169
171
 
@@ -243,6 +245,17 @@ module GraphQL
243
245
 
244
246
  result
245
247
  end
248
+
249
+ private
250
+
251
+ # Suppress redefinition warning for objectId arguments
252
+ def suppress_redefinition_warning
253
+ verbose = $VERBOSE
254
+ $VERBOSE = nil
255
+ yield
256
+ ensure
257
+ $VERBOSE = verbose
258
+ end
246
259
  end
247
260
 
248
261
  private
@@ -135,7 +135,7 @@ module GraphQL
135
135
 
136
136
  def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
137
137
  warden = Warden.from_context(context)
138
- skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Subset)
138
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
139
139
  for ancestor in ancestors
140
140
  if ancestor.respond_to?(:own_arguments) &&
141
141
  (a = ancestor.own_arguments[argument_name]) &&
@@ -210,7 +210,7 @@ module GraphQL
210
210
  # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name.
211
211
  def get_argument(argument_name, context = GraphQL::Query::NullContext.instance)
212
212
  warden = Warden.from_context(context)
213
- if (arg_config = own_arguments[argument_name]) && ((context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Subset)) || (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden)))
213
+ if (arg_config = own_arguments[argument_name]) && ((context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)) || (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden)))
214
214
  visible_arg || arg_config
215
215
  elsif defined?(@resolver_class) && @resolver_class
216
216
  @resolver_class.get_field_argument(argument_name, context)
@@ -99,7 +99,7 @@ module GraphQL
99
99
  module InterfaceMethods
100
100
  def get_field(field_name, context = GraphQL::Query::NullContext.instance)
101
101
  warden = Warden.from_context(context)
102
- skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Subset)
102
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
103
103
  for ancestor in ancestors
104
104
  if ancestor.respond_to?(:own_fields) &&
105
105
  (f_entry = ancestor.own_fields[field_name]) &&
@@ -135,7 +135,7 @@ module GraphQL
135
135
  # Objects need to check that the interface implementation is visible, too
136
136
  warden = Warden.from_context(context)
137
137
  ancs = ancestors
138
- skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Subset)
138
+ skip_visible = context.respond_to?(:types) && context.types.is_a?(GraphQL::Schema::Visibility::Profile)
139
139
  i = 0
140
140
  while (ancestor = ancs[i])
141
141
  if ancestor.respond_to?(:own_fields) &&
@@ -58,6 +58,7 @@ module GraphQL
58
58
  end
59
59
  end
60
60
  schema = Class.new(GraphQL::Schema) {
61
+ use GraphQL::Schema::Visibility
61
62
  query(query_root)
62
63
  def self.visible?(member, _ctx)
63
64
  member.graphql_name != "Root"
@@ -35,9 +35,10 @@ module GraphQL
35
35
  # end
36
36
  #
37
37
  class RequiredValidator < Validator
38
- # @param one_of [Symbol, Array<Symbol>] An argument, or a list of arguments, that represents a valid set of inputs for this field
38
+ # @param one_of [Array<Symbol>] A list of arguments, exactly one of which is required for this field
39
+ # @param argument [Symbol] An argument that is required for this field
39
40
  # @param message [String]
40
- def initialize(one_of: nil, argument: nil, message: "%{validated} has the wrong arguments", **default_options)
41
+ def initialize(one_of: nil, argument: nil, message: nil, **default_options)
41
42
  @one_of = if one_of
42
43
  one_of
43
44
  elsif argument
@@ -49,7 +50,7 @@ module GraphQL
49
50
  super(**default_options)
50
51
  end
51
52
 
52
- def validate(_object, _context, value)
53
+ def validate(_object, context, value)
53
54
  matched_conditions = 0
54
55
 
55
56
  if !value.nil?
@@ -73,9 +74,32 @@ module GraphQL
73
74
  if matched_conditions == 1
74
75
  nil # OK
75
76
  else
76
- @message
77
+ @message || build_message(context)
77
78
  end
78
79
  end
80
+
81
+ def build_message(context)
82
+ argument_definitions = @validated.arguments(context).values
83
+ required_names = @one_of.map do |arg_keyword|
84
+ if arg_keyword.is_a?(Array)
85
+ names = arg_keyword.map { |arg| arg_keyword_to_grapqhl_name(argument_definitions, arg) }
86
+ "(" + names.join(" and ") + ")"
87
+ else
88
+ arg_keyword_to_grapqhl_name(argument_definitions, arg_keyword)
89
+ end
90
+ end
91
+
92
+ if required_names.size == 1
93
+ "%{validated} must include the following argument: #{required_names.first}."
94
+ else
95
+ "%{validated} must include exactly one of the following arguments: #{required_names.join(", ")}."
96
+ end
97
+ end
98
+
99
+ def arg_keyword_to_grapqhl_name(argument_definitions, arg_keyword)
100
+ argument_definition = argument_definitions.find { |defn| defn.keyword == arg_keyword }
101
+ argument_definition.graphql_name
102
+ end
79
103
  end
80
104
  end
81
105
  end
@@ -2,7 +2,7 @@
2
2
  module GraphQL
3
3
  class Schema
4
4
  class Visibility
5
- # You can use this to see how {GraphQL::Schema::Warden} and {GraphQL::Schema::Visibility::Subset}
5
+ # You can use this to see how {GraphQL::Schema::Warden} and {GraphQL::Schema::Visibility::Profile}
6
6
  # handle `.visible?` differently in your schema.
7
7
  #
8
8
  # It runs the same method on both implementations and raises an error when the results diverge.
@@ -15,28 +15,24 @@ module GraphQL
15
15
  # This plugin adds two keys to `context` when running:
16
16
  #
17
17
  # - `visibility_migration_running: true`
18
- # - For the {Warden} which it instantiates, it adds `visibility_migration_warden_running: true`.
18
+ # - For the {Schema::Warden} which it instantiates, it adds `visibility_migration_warden_running: true`.
19
19
  #
20
20
  # Use those keys to modify your `visible?` behavior as needed.
21
21
  #
22
22
  # Also, in a pinch, you can set `skip_visibility_migration_error: true` in context to turn off this behavior per-query.
23
- # (In that case, it uses {Subset} directly.)
23
+ # (In that case, it uses {Profile} directly.)
24
24
  #
25
25
  # @example Adding this plugin
26
26
  #
27
- # use GraphQL::Schema::Visibility::Migration
27
+ # use GraphQL::Schema::Visibility, migration_errors: true
28
28
  #
29
- class Migration < GraphQL::Schema::Visibility::Subset
30
- def self.use(schema)
31
- schema.subset_class = self
32
- end
33
-
29
+ class Migration < GraphQL::Schema::Visibility::Profile
34
30
  class RuntimeTypesMismatchError < GraphQL::Error
35
- def initialize(method_called, warden_result, subset_result, method_args)
31
+ def initialize(method_called, warden_result, profile_result, method_args)
36
32
  super(<<~ERR)
37
33
  Mismatch in types for `##{method_called}(#{method_args.map(&:inspect).join(", ")})`:
38
34
 
39
- #{compare_results(warden_result, subset_result)}
35
+ #{compare_results(warden_result, profile_result)}
40
36
 
41
37
  Update your `.visible?` implementation to make these implementations return the same value.
42
38
 
@@ -45,9 +41,9 @@ module GraphQL
45
41
  end
46
42
 
47
43
  private
48
- def compare_results(warden_result, subset_result)
49
- if warden_result.is_a?(Array) && subset_result.is_a?(Array)
50
- all_results = warden_result | subset_result
44
+ def compare_results(warden_result, profile_result)
45
+ if warden_result.is_a?(Array) && profile_result.is_a?(Array)
46
+ all_results = warden_result | profile_result
51
47
  all_results.sort_by!(&:graphql_name)
52
48
 
53
49
  entries_text = all_results.map { |entry| "#{entry.graphql_name} (#{entry})"}
@@ -55,13 +51,13 @@ module GraphQL
55
51
  yes = " ✔ "
56
52
  no = " "
57
53
  res = "".dup
58
- res << "#{"Result".center(width)} Warden Subset \n"
54
+ res << "#{"Result".center(width)} Warden Profile \n"
59
55
  all_results.each_with_index do |entry, idx|
60
- res << "#{entries_text[idx].ljust(width)}#{warden_result.include?(entry) ? yes : no}#{subset_result.include?(entry) ? yes : no}\n"
56
+ res << "#{entries_text[idx].ljust(width)}#{warden_result.include?(entry) ? yes : no}#{profile_result.include?(entry) ? yes : no}\n"
61
57
  end
62
58
  res << "\n"
63
59
  else
64
- "- Warden returned: #{humanize(warden_result)}\n\n- Subset returned: #{humanize(subset_result)}"
60
+ "- Warden returned: #{humanize(warden_result)}\n\n- Visibility::Profile returned: #{humanize(profile_result)}"
65
61
  end
66
62
  end
67
63
  def humanize(val)
@@ -80,38 +76,40 @@ module GraphQL
80
76
  end
81
77
  end
82
78
 
83
- def initialize(context:, schema:)
84
- @skip_error = context[:skip_visibility_migration_error]
85
- context[:visibility_migration_running] = true
86
- @subset_types = GraphQL::Schema::Visibility::Subset.new(context: context, schema: schema)
79
+ def initialize(context:, schema:, name: nil)
80
+ @name = name
81
+ @skip_error = context[:skip_visibility_migration_error] || context.is_a?(Query::NullContext) || context.is_a?(Hash)
82
+ @profile_types = GraphQL::Schema::Visibility::Profile.new(context: context, schema: schema)
87
83
  if !@skip_error
84
+ context[:visibility_migration_running] = true
88
85
  warden_ctx_vals = context.to_h.dup
89
86
  warden_ctx_vals[:visibility_migration_warden_running] = true
90
- if defined?(schema::WardenCompatSchema)
91
- warden_schema = schema::WardenCompatSchema
87
+ if schema.const_defined?(:WardenCompatSchema, false) # don't use a defn from a superclass
88
+ warden_schema = schema.const_get(:WardenCompatSchema, false)
92
89
  else
93
90
  warden_schema = Class.new(schema)
94
- warden_schema.use_schema_visibility = false
91
+ warden_schema.use_visibility_profile = false
95
92
  # TODO public API
96
93
  warden_schema.send(:add_type_and_traverse, [warden_schema.query, warden_schema.mutation, warden_schema.subscription].compact, root: true)
97
94
  warden_schema.send(:add_type_and_traverse, warden_schema.directives.values + warden_schema.orphan_types, root: false)
95
+ schema.const_set(:WardenCompatSchema, warden_schema)
98
96
  end
99
97
  warden_ctx = GraphQL::Query::Context.new(query: context.query, values: warden_ctx_vals)
100
- example_warden = GraphQL::Schema::Warden.new(schema: warden_schema, context: warden_ctx)
101
- @warden_types = example_warden.schema_subset
102
- warden_ctx.warden = example_warden
103
- warden_ctx.types = @warden_types
98
+ warden_ctx.warden = GraphQL::Schema::Warden.new(schema: warden_schema, context: warden_ctx)
99
+ warden_ctx.warden.skip_warning = true
100
+ warden_ctx.types = @warden_types = warden_ctx.warden.visibility_profile
104
101
  end
105
102
  end
106
103
 
107
104
  def loaded_types
108
- @subset_types.loaded_types
105
+ @profile_types.loaded_types
109
106
  end
110
107
 
111
- PUBLIC_SUBSET_METHODS = [
108
+ PUBLIC_PROFILE_METHODS = [
112
109
  :enum_values,
113
110
  :interfaces,
114
111
  :all_types,
112
+ :all_types_h,
115
113
  :fields,
116
114
  :loadable?,
117
115
  :type,
@@ -124,17 +122,18 @@ module GraphQL
124
122
  :mutation_root,
125
123
  :possible_types,
126
124
  :subscription_root,
127
- :reachable_type?
125
+ :reachable_type?,
126
+ :visible_enum_value?,
128
127
  ]
129
128
 
130
- PUBLIC_SUBSET_METHODS.each do |subset_method|
131
- define_method(subset_method) do |*args|
132
- call_method_and_compare(subset_method, args)
129
+ PUBLIC_PROFILE_METHODS.each do |profile_method|
130
+ define_method(profile_method) do |*args|
131
+ call_method_and_compare(profile_method, args)
133
132
  end
134
133
  end
135
134
 
136
135
  def call_method_and_compare(method, args)
137
- res_1 = @subset_types.public_send(method, *args)
136
+ res_1 = @profile_types.public_send(method, *args)
138
137
  if @skip_error
139
138
  return res_1
140
139
  end