graphql 1.12.13 → 1.12.17

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cad71b0084305ae219dd00e6c6ed48871f126d90c38cf47b9708db1eaf1feb6
4
- data.tar.gz: afe00c0b1f14134068015098392817c0c0f0841f8016863789d319590dc5a9ba
3
+ metadata.gz: 6c9089e4578454f473996553a4771e4086e51b2b2db17fc31a579ab28019bd39
4
+ data.tar.gz: f45a70c81394f35c86d1610491f106e39899bc2c927a6a6d13e9e6533bc3bc85
5
5
  SHA512:
6
- metadata.gz: 8fc7bee0a1a2bdc836358338d41d56c394f495dcc78d17403c60efd47ae8f807ad447fcb96a9c270137fa247fde43b5936961436efb6d1276768f945bd5be077
7
- data.tar.gz: a0da8f32b54df7b1254dac3749ffe1e9a9b2d93ec273313280d755b5c2bf3910880f2a13f4861d7efb0b2e39a5aefc63491918a89bcdfa223893769a0a9a1ddf
6
+ metadata.gz: b7231b5e00a336439d15d469a383cd7e211e305961d4a4b431e8a97f5e483819bbd87de3d7cfffc9b6677b8175f700e012cd9ee4a0c92fa47005455a14893d3c
7
+ data.tar.gz: 98495c2b24d65ef90d7f5e1036956349ea79d03ea192de6ed06e517657f49b4e0e9c748c8a28d54aac24e96b42f52eea5d4cbfbca801a4389b1515804296831a
@@ -7,6 +7,7 @@ module GraphQL
7
7
  super
8
8
  @used_fields = Set.new
9
9
  @used_deprecated_fields = Set.new
10
+ @used_deprecated_arguments = Set.new
10
11
  end
11
12
 
12
13
  def on_leave_field(node, parent, visitor)
@@ -14,14 +15,36 @@ module GraphQL
14
15
  field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
15
16
  @used_fields << field
16
17
  @used_deprecated_fields << field if field_defn.deprecation_reason
18
+
19
+ extract_deprecated_arguments(visitor.query.arguments_for(node, visitor.field_definition).argument_values)
17
20
  end
18
21
 
19
22
  def result
20
23
  {
21
24
  used_fields: @used_fields.to_a,
22
- used_deprecated_fields: @used_deprecated_fields.to_a
25
+ used_deprecated_fields: @used_deprecated_fields.to_a,
26
+ used_deprecated_arguments: @used_deprecated_arguments.to_a,
23
27
  }
24
28
  end
29
+
30
+ private
31
+
32
+ def extract_deprecated_arguments(argument_values)
33
+ argument_values.each_pair do |_argument_name, argument|
34
+ if argument.definition.deprecation_reason
35
+ @used_deprecated_arguments << argument.definition.path
36
+ end
37
+
38
+ if argument.definition.type.kind.input_object?
39
+ extract_deprecated_arguments(argument.value.arguments.argument_values)
40
+ elsif argument.definition.type.list?
41
+ argument
42
+ .value
43
+ .select { |value| value.respond_to?(:arguments) }
44
+ .each { |value| extract_deprecated_arguments(value.arguments.argument_values) }
45
+ end
46
+ end
47
+ end
25
48
  end
26
49
  end
27
50
  end
@@ -15,10 +15,13 @@ module GraphQL
15
15
  # No query context yet
16
16
  nil
17
17
  when "validate", "analyze_query", "execute_query", "execute_query_lazy"
18
- query = metadata[:query] || metadata[:queries].first
19
18
  push_key = []
20
- push_data = query
21
- multiplex = query.multiplex
19
+ if (query = metadata[:query]) || ((queries = metadata[:queries]) && (query = queries.first))
20
+ push_data = query
21
+ multiplex = query.multiplex
22
+ elsif (multiplex = metadata[:multiplex])
23
+ push_data = multiplex.queries.first
24
+ end
22
25
  when "execute_field", "execute_field_lazy"
23
26
  query = metadata[:query] || raise(ArgumentError, "Add `legacy: true` to use GraphQL::Backtrace without the interpreter runtime.")
24
27
  multiplex = query.multiplex
@@ -89,6 +89,24 @@ module GraphQL
89
89
  nil
90
90
  end
91
91
 
92
+ # These arguments are given to `dataloader.with(source_class, ...)`. The object
93
+ # returned from this method is used to de-duplicate batch loads under the hood
94
+ # by using it as a Hash key.
95
+ #
96
+ # By default, the arguments are all put in an Array. To customize how this source's
97
+ # batches are merged, override this method to return something else.
98
+ #
99
+ # For example, if you pass `ActiveRecord::Relation`s to `.with(...)`, you could override
100
+ # this method to call `.to_sql` on them, thus merging `.load(...)` calls when they apply
101
+ # to equivalent relations.
102
+ #
103
+ # @param batch_args [Array<Object>]
104
+ # @param batch_kwargs [Hash]
105
+ # @return [Object]
106
+ def self.batch_key_for(*batch_args, **batch_kwargs)
107
+ [*batch_args, **batch_kwargs]
108
+ end
109
+
92
110
  private
93
111
 
94
112
  # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
@@ -27,18 +27,20 @@ module GraphQL
27
27
  schema.dataloader_class = self
28
28
  end
29
29
 
30
- def initialize
31
- @source_cache = Hash.new { |h, source_class| h[source_class] = Hash.new { |h2, batch_parameters|
32
- source = if RUBY_VERSION < "3"
33
- source_class.new(*batch_parameters)
34
- else
35
- batch_args, batch_kwargs = batch_parameters
36
- source_class.new(*batch_args, **batch_kwargs)
37
- end
38
- source.setup(self)
39
- h2[batch_parameters] = source
40
- }
30
+ # Call the block with a Dataloader instance,
31
+ # then run all enqueued jobs and return the result of the block.
32
+ def self.with_dataloading(&block)
33
+ dataloader = self.new
34
+ result = nil
35
+ dataloader.append_job {
36
+ result = block.call(dataloader)
41
37
  }
38
+ dataloader.run
39
+ result
40
+ end
41
+
42
+ def initialize
43
+ @source_cache = Hash.new { |h, k| h[k] = {} }
42
44
  @pending_jobs = []
43
45
  end
44
46
 
@@ -49,16 +51,24 @@ module GraphQL
49
51
  # @return [GraphQL::Dataloader::Source] An instance of {source_class}, initialized with `self, *batch_parameters`,
50
52
  # and cached for the lifetime of this {Multiplex}.
51
53
  if RUBY_VERSION < "3"
52
- def with(source_class, *batch_parameters)
53
- @source_cache[source_class][batch_parameters]
54
+ def with(source_class, *batch_args)
55
+ batch_key = source_class.batch_key_for(*batch_args)
56
+ @source_cache[source_class][batch_key] ||= begin
57
+ source = source_class.new(*batch_args)
58
+ source.setup(self)
59
+ source
60
+ end
54
61
  end
55
62
  else
56
63
  def with(source_class, *batch_args, **batch_kwargs)
57
- batch_parameters = [batch_args, batch_kwargs]
58
- @source_cache[source_class][batch_parameters]
64
+ batch_key = source_class.batch_key_for(*batch_args, **batch_kwargs)
65
+ @source_cache[source_class][batch_key] ||= begin
66
+ source = source_class.new(*batch_args, **batch_kwargs)
67
+ source.setup(self)
68
+ source
69
+ end
59
70
  end
60
71
  end
61
-
62
72
  # Tell the dataloader that this fiber is waiting for data.
63
73
  #
64
74
  # Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
@@ -76,7 +76,7 @@ ERR
76
76
  # Apply definition from `define(...)` kwargs
77
77
  defn.define_keywords.each do |keyword, value|
78
78
  # Don't splat string hashes, which blows up on Rubies before 2.7
79
- if value.is_a?(Hash) && value.each_key.all? { |k| k.is_a?(Symbol) }
79
+ if value.is_a?(Hash) && !value.empty? && value.each_key.all? { |k| k.is_a?(Symbol) }
80
80
  defn_proxy.public_send(keyword, **value)
81
81
  else
82
82
  defn_proxy.public_send(keyword, value)
@@ -10,7 +10,19 @@ module GraphQL
10
10
  class Runtime
11
11
 
12
12
  module GraphQLResult
13
- attr_accessor :graphql_dead, :graphql_parent, :graphql_result_name
13
+ def initialize(result_name, parent_result)
14
+ @graphql_parent = parent_result
15
+ if parent_result && parent_result.graphql_dead
16
+ @graphql_dead = true
17
+ end
18
+ @graphql_result_name = result_name
19
+ # Jump through some hoops to avoid creating this duplicate storage if at all possible.
20
+ @graphql_metadata = nil
21
+ end
22
+
23
+ attr_accessor :graphql_dead
24
+ attr_reader :graphql_parent, :graphql_result_name
25
+
14
26
  # Although these are used by only one of the Result classes,
15
27
  # it's handy to have the methods implemented on both (even though they just return `nil`)
16
28
  # because it makes it easy to check if anything is assigned.
@@ -24,9 +36,8 @@ module GraphQL
24
36
  end
25
37
 
26
38
  class GraphQLResultHash
27
- def initialize
28
- # Jump through some hoops to avoid creating this duplicate hash if at all possible.
29
- @graphql_metadata = nil
39
+ def initialize(_result_name, _parent_result)
40
+ super
30
41
  @graphql_result_data = {}
31
42
  end
32
43
 
@@ -86,10 +97,8 @@ module GraphQL
86
97
  class GraphQLResultArray
87
98
  include GraphQLResult
88
99
 
89
- def initialize
90
- # Avoid this duplicate allocation if possible -
91
- # but it will require some work to keep it up-to-date if it's created.
92
- @graphql_metadata = nil
100
+ def initialize(_result_name, _parent_result)
101
+ super
93
102
  @graphql_result_data = []
94
103
  end
95
104
 
@@ -146,7 +155,7 @@ module GraphQL
146
155
  @context = query.context
147
156
  @multiplex_context = query.multiplex.context
148
157
  @interpreter_context = @context.namespace(:interpreter)
149
- @response = GraphQLResultHash.new
158
+ @response = GraphQLResultHash.new(nil, nil)
150
159
  # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
151
160
  @runtime_directive_names = []
152
161
  noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
@@ -208,7 +217,7 @@ module GraphQL
208
217
  # directly evaluated and the results can be written right into the main response hash.
209
218
  tap_or_each(gathered_selections) do |selections, is_selection_array|
210
219
  if is_selection_array
211
- selection_response = GraphQLResultHash.new
220
+ selection_response = GraphQLResultHash.new(nil, nil)
212
221
  final_response = @response
213
222
  else
214
223
  selection_response = @response
@@ -227,6 +236,7 @@ module GraphQL
227
236
  selections,
228
237
  selection_response,
229
238
  final_response,
239
+ nil,
230
240
  )
231
241
  end
232
242
  }
@@ -338,7 +348,7 @@ module GraphQL
338
348
  NO_ARGS = {}.freeze
339
349
 
340
350
  # @return [void]
341
- def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result) # rubocop:disable Metrics/ParameterLists
351
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections, selections_result, target_result, parent_object) # rubocop:disable Metrics/ParameterLists
342
352
  set_all_interpreter_context(owner_object, nil, nil, path)
343
353
 
344
354
  finished_jobs = 0
@@ -346,7 +356,7 @@ module GraphQL
346
356
  gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
347
357
  @dataloader.append_job {
348
358
  evaluate_selection(
349
- path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result
359
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection, selections_result, parent_object
350
360
  )
351
361
  finished_jobs += 1
352
362
  if target_result && finished_jobs == enqueued_jobs
@@ -361,7 +371,8 @@ module GraphQL
361
371
  attr_reader :progress_path
362
372
 
363
373
  # @return [void]
364
- def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field, selections_result) # rubocop:disable Metrics/ParameterLists
374
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field, selections_result, parent_object) # rubocop:disable Metrics/ParameterLists
375
+ return if dead_result?(selections_result)
365
376
  # As a performance optimization, the hash key will be a `Node` if
366
377
  # there's only one selection of the field. But if there are multiple
367
378
  # selections of the field, it will be an Array of nodes
@@ -411,16 +422,16 @@ module GraphQL
411
422
  total_args_count = field_defn.arguments.size
412
423
  if total_args_count == 0
413
424
  kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
414
- evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
425
+ evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result, parent_object)
415
426
  else
416
427
  # TODO remove all arguments(...) usages?
417
428
  @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
418
- evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result)
429
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selections_result, parent_object)
419
430
  end
420
431
  end
421
432
  end
422
433
 
423
- def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
434
+ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field, result_name, selection_result, parent_object) # rubocop:disable Metrics/ParameterLists
424
435
  context.scoped_context = scoped_context
425
436
  return_type = field_defn.type
426
437
  after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments, result_name: result_name, result: selection_result) do |resolved_arguments|
@@ -462,6 +473,8 @@ module GraphQL
462
473
  # This is used by `__typename` in order to support the legacy runtime,
463
474
  # but it has no use here (and it's always `nil`).
464
475
  # Stop adding it here to avoid the overhead of `.merge_extras` below.
476
+ when :parent
477
+ extra_args[:parent] = parent_object
465
478
  else
466
479
  extra_args[extra] = field_defn.fetch_extra(extra, context)
467
480
  end
@@ -693,9 +706,7 @@ module GraphQL
693
706
  after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false, result_name: result_name, result: selection_result) do |inner_object|
694
707
  continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
695
708
  if HALT != continue_value
696
- response_hash = GraphQLResultHash.new
697
- response_hash.graphql_parent = selection_result
698
- response_hash.graphql_result_name = result_name
709
+ response_hash = GraphQLResultHash.new(result_name, selection_result)
699
710
  set_result(selection_result, result_name, response_hash)
700
711
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
701
712
  # There are two possibilities for `gathered_selections`:
@@ -708,9 +719,7 @@ module GraphQL
708
719
  # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
709
720
  tap_or_each(gathered_selections) do |selections, is_selection_array|
710
721
  if is_selection_array
711
- this_result = GraphQLResultHash.new
712
- this_result.graphql_parent = selection_result
713
- this_result.graphql_result_name = result_name
722
+ this_result = GraphQLResultHash.new(result_name, selection_result)
714
723
  final_result = response_hash
715
724
  else
716
725
  this_result = response_hash
@@ -727,6 +736,7 @@ module GraphQL
727
736
  selections,
728
737
  this_result,
729
738
  final_result,
739
+ owner_object.object,
730
740
  )
731
741
  this_result
732
742
  end
@@ -735,16 +745,15 @@ module GraphQL
735
745
  end
736
746
  when "LIST"
737
747
  inner_type = current_type.of_type
738
- response_list = GraphQLResultArray.new
748
+ response_list = GraphQLResultArray.new(result_name, selection_result)
739
749
  response_list.graphql_non_null_list_items = inner_type.non_null?
740
- response_list.graphql_parent = selection_result
741
- response_list.graphql_result_name = result_name
742
750
  set_result(selection_result, result_name, response_list)
743
751
 
744
752
  idx = 0
745
753
  scoped_context = context.scoped_context
746
754
  begin
747
755
  value.each do |inner_value|
756
+ break if dead_result?(response_list)
748
757
  next_path = path.dup
749
758
  next_path << idx
750
759
  this_idx = idx
@@ -23,6 +23,12 @@ module GraphQL
23
23
  if value.nil?
24
24
  'null'
25
25
  else
26
+ if (@object.type.kind.list? || (@object.type.kind.non_null? && @object.type.of_type.kind.list?)) && !value.respond_to?(:map)
27
+ # This is a bit odd -- we expect the default value to be an application-style value, so we use coerce result below.
28
+ # But coerce_result doesn't wrap single-item lists, which are valid inputs to list types.
29
+ # So, apply that wrapper here if needed.
30
+ value = [value]
31
+ end
26
32
  coerced_default_value = @object.type.coerce_result(value, @context)
27
33
  serialize_default_value(coerced_default_value, @object.type)
28
34
  end
@@ -130,6 +130,11 @@ module GraphQL
130
130
  if defined?(Mongoid::Association::Referenced::HasMany::Targets::Enumerable)
131
131
  add(Mongoid::Association::Referenced::HasMany::Targets::Enumerable, Pagination::MongoidRelationConnection)
132
132
  end
133
+
134
+ # Mongoid 7.3+
135
+ if defined?(Mongoid::Association::Referenced::HasMany::Enumerable)
136
+ add(Mongoid::Association::Referenced::HasMany::Enumerable, Pagination::MongoidRelationConnection)
137
+ end
133
138
  end
134
139
  end
135
140
  end
data/lib/graphql/query.rb CHANGED
@@ -117,6 +117,10 @@ module GraphQL
117
117
  raise ArgumentError, "Query should only be provided a query string or a document, not both."
118
118
  end
119
119
 
120
+ if @query_string && !@query_string.is_a?(String)
121
+ raise ArgumentError, "Query string argument should be a String, got #{@query_string.class.name} instead."
122
+ end
123
+
120
124
  # A two-layer cache of type resolution:
121
125
  # { abstract_type => { value => resolved_type } }
122
126
  @resolved_types_cache = Hash.new do |h1, k1|
@@ -270,7 +274,7 @@ module GraphQL
270
274
  # @return [String, nil] Returns nil if the query is invalid.
271
275
  def sanitized_query_string(inline_variables: true)
272
276
  with_prepared_ast {
273
- GraphQL::Language::SanitizedPrinter.new(self, inline_variables: inline_variables).sanitized_query_string
277
+ schema.sanitized_printer.new(self, inline_variables: inline_variables).sanitized_query_string
274
278
  }
275
279
  end
276
280
 
@@ -151,7 +151,7 @@ module GraphQL
151
151
  input_obj_arg = input_obj_arg.type_class
152
152
  # TODO: this skips input objects whose values were alread replaced with application objects.
153
153
  # See: https://github.com/rmosolgo/graphql-ruby/issues/2633
154
- if value.respond_to?(:key?) && value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
154
+ if value.is_a?(InputObject) && value.key?(input_obj_arg.keyword) && !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
155
155
  return false
156
156
  end
157
157
  end
@@ -298,7 +298,21 @@ module GraphQL
298
298
  # @api private
299
299
  def validate_default_value
300
300
  coerced_default_value = begin
301
- type.coerce_isolated_result(default_value) unless default_value.nil?
301
+ # This is weird, but we should accept single-item default values for list-type arguments.
302
+ # If we used `coerce_isolated_input` below, it would do this for us, but it's not really
303
+ # the right thing here because we expect default values in application format (Ruby values)
304
+ # not GraphQL format (scalar values).
305
+ #
306
+ # But I don't think Schema::List#coerce_result should apply wrapping to single-item lists.
307
+ prepped_default_value = if default_value.nil?
308
+ nil
309
+ elsif (type.kind.list? || (type.kind.non_null? && type.of_type.list?)) && !default_value.respond_to?(:map)
310
+ [default_value]
311
+ else
312
+ default_value
313
+ end
314
+
315
+ type.coerce_isolated_result(prepped_default_value) unless prepped_default_value.nil?
302
316
  rescue GraphQL::Schema::Enum::UnresolvedValueError
303
317
  # It raises this, which is helpful at runtime, but not here...
304
318
  default_value
@@ -47,10 +47,16 @@ module GraphQL
47
47
  # _while_ building the schema.
48
48
  # It will dig for a type if it encounters a custom type. This could be a problem if there are cycles.
49
49
  directive_type_resolver = nil
50
- directive_type_resolver = build_resolve_type(GraphQL::Schema::BUILT_IN_TYPES, directives, ->(type_name) {
50
+ directive_type_resolver = build_resolve_type(types, directives, ->(type_name) {
51
51
  types[type_name] ||= begin
52
52
  defn = document.definitions.find { |d| d.respond_to?(:name) && d.name == type_name }
53
- build_definition_from_node(defn, directive_type_resolver, default_resolve)
53
+ if defn
54
+ build_definition_from_node(defn, directive_type_resolver, default_resolve)
55
+ elsif (built_in_defn = GraphQL::Schema::BUILT_IN_TYPES[type_name])
56
+ built_in_defn
57
+ else
58
+ raise "No definition for #{type_name.inspect} found in schema document or built-in types. Add a definition for it or remove it."
59
+ end
54
60
  end
55
61
  })
56
62
 
@@ -71,11 +71,11 @@ module GraphQL
71
71
  end
72
72
 
73
73
  def prepare
74
- if context
75
- context.schema.after_any_lazies(@maybe_lazies) do
76
- object = context[:current_object]
74
+ if @context
75
+ @context.schema.after_any_lazies(@maybe_lazies) do
76
+ object = @context[:current_object]
77
77
  # Pass this object's class with `as` so that messages are rendered correctly from inherited validators
78
- Schema::Validator.validate!(self.class.validators, object, context, @ruby_style_hash, as: self.class)
78
+ Schema::Validator.validate!(self.class.validators, object, @context, @ruby_style_hash, as: self.class)
79
79
  self
80
80
  end
81
81
  else
@@ -124,6 +124,7 @@ module GraphQL
124
124
  end
125
125
 
126
126
  def camelize(string)
127
+ return string if string == '_'
127
128
  return string unless string.include?("_")
128
129
  camelized = string.split('_').map(&:capitalize).join
129
130
  camelized[0] = camelized[0].downcase
@@ -218,8 +218,10 @@ module GraphQL
218
218
  own_extras + (superclass.respond_to?(:extras) ? superclass.extras : [])
219
219
  end
220
220
 
221
- # Specifies whether or not the field is nullable. Defaults to `true`
222
- # TODO unify with {#type}
221
+ # If `true` (default), then the return type for this resolver will be nullable.
222
+ # If `false`, then the return type is non-null.
223
+ #
224
+ # @see #type which sets the return type of this field and accepts a `null:` option
223
225
  # @param allow_null [Boolean] Whether or not the response can be null
224
226
  def null(allow_null = nil)
225
227
  if !allow_null.nil?
@@ -58,11 +58,9 @@ module GraphQL
58
58
  end
59
59
  end
60
60
 
61
- # Default implementation returns the root object.
61
+ # The default implementation returns nothing on subscribe.
62
62
  # Override it to return an object or
63
- # `:no_response` to return nothing.
64
- #
65
- # The default is `:no_response`.
63
+ # `:no_response` to (explicitly) return nothing.
66
64
  def subscribe(args = {})
67
65
  :no_response
68
66
  end
@@ -116,6 +114,26 @@ module GraphQL
116
114
  end
117
115
  end
118
116
 
117
+ # This is called during initial subscription to get a "name" for this subscription.
118
+ # Later, when `.trigger` is called, this will be called again to build another "name".
119
+ # Any subscribers with matching topic will begin the update flow.
120
+ #
121
+ # The default implementation creates a string using the field name, subscription scope, and argument keys and values.
122
+ # In that implementation, only `.trigger` calls with _exact matches_ result in updates to subscribers.
123
+ #
124
+ # To implement a filtered stream-type subscription flow, override this method to return a string with field name and subscription scope.
125
+ # Then, implement {#update} to compare its arguments to the current `object` and return `:no_update` when an
126
+ # update should be filtered out.
127
+ #
128
+ # @see {#update} for how to skip updates when an event comes with a matching topic.
129
+ # @param arguments [Hash<String => Object>] The arguments for this topic, in GraphQL-style (camelized strings)
130
+ # @param field [GraphQL::Schema::Field]
131
+ # @param scope [Object, nil] A value corresponding to `.trigger(... scope:)` (for updates) or the `subscription_scope` found in `context` (for initial subscriptions).
132
+ # @return [String] An identifier corresponding to a stream of updates
133
+ def self.topic_for(arguments:, field:, scope:)
134
+ Subscriptions::Serialize.dump_recursive([scope, field.graphql_name, arguments])
135
+ end
136
+
119
137
  # Overriding Resolver#field_options to include subscription_scope
120
138
  def self.field_options
121
139
  super.merge(
@@ -24,12 +24,13 @@ module GraphQL
24
24
  # @param other_than [Integer]
25
25
  # @param odd [Boolean]
26
26
  # @param even [Boolean]
27
+ # @param within [Range]
27
28
  # @param message [String] used for all validation failures
28
29
  def initialize(
29
30
  greater_than: nil, greater_than_or_equal_to: nil,
30
31
  less_than: nil, less_than_or_equal_to: nil,
31
32
  equal_to: nil, other_than: nil,
32
- odd: nil, even: nil,
33
+ odd: nil, even: nil, within: nil,
33
34
  message: "%{validated} must be %{comparison} %{target}",
34
35
  **default_options
35
36
  )
@@ -42,6 +43,7 @@ module GraphQL
42
43
  @other_than = other_than
43
44
  @odd = odd
44
45
  @even = even
46
+ @within = within
45
47
  @message = message
46
48
  super(**default_options)
47
49
  end
@@ -63,6 +65,8 @@ module GraphQL
63
65
  (partial_format(@message, { comparison: "even", target: "" })).strip
64
66
  elsif @odd && !value.odd?
65
67
  (partial_format(@message, { comparison: "odd", target: "" })).strip
68
+ elsif @within && !@within.include?(value)
69
+ partial_format(@message, { comparison: "within", target: @within })
66
70
  end
67
71
  end
68
72
  end
@@ -1631,6 +1631,14 @@ module GraphQL
1631
1631
  find_inherited_value(:multiplex_analyzers, EMPTY_ARRAY) + own_multiplex_analyzers
1632
1632
  end
1633
1633
 
1634
+ def sanitized_printer(new_sanitized_printer = nil)
1635
+ if new_sanitized_printer
1636
+ @own_sanitized_printer = new_sanitized_printer
1637
+ else
1638
+ @own_sanitized_printer || GraphQL::Language::SanitizedPrinter
1639
+ end
1640
+ end
1641
+
1634
1642
  # Execute a query on itself.
1635
1643
  # @see {Query#initialize} for arguments.
1636
1644
  # @return [Hash] query result, ready to be serialized as JSON
@@ -95,6 +95,14 @@ module GraphQL
95
95
  @action_cable = action_cable
96
96
  @action_cable_coder = action_cable_coder
97
97
  @serializer = serializer
98
+ @serialize_with_context = case @serializer.method(:load).arity
99
+ when 1
100
+ false
101
+ when 2
102
+ true
103
+ else
104
+ raise ArgumentError, "#{@serializer} must repond to `.load` accepting one or two arguments"
105
+ end
98
106
  @transmit_ns = namespace
99
107
  super
100
108
  end
@@ -154,7 +162,7 @@ module GraphQL
154
162
  # so just run it once, then deliver the result to every subscriber
155
163
  first_event = events.first
156
164
  first_subscription_id = first_event.context.fetch(:subscription_id)
157
- object ||= @serializer.load(message)
165
+ object ||= load_action_cable_message(message, first_event.context)
158
166
  result = execute_update(first_subscription_id, first_event, object)
159
167
  # Having calculated the result _once_, send the same payload to all subscribers
160
168
  events.each do |event|
@@ -167,6 +175,18 @@ module GraphQL
167
175
  end
168
176
  end
169
177
 
178
+ # This is called to turn an ActionCable-broadcasted string (JSON)
179
+ # into a query-ready application object.
180
+ # @param message [String] n ActionCable-broadcasted string (JSON)
181
+ # @param context [GraphQL::Query::Context] the context of the first event for a given subscription fingerprint
182
+ def load_action_cable_message(message, context)
183
+ if @serialize_with_context
184
+ @serializer.load(message, context)
185
+ else
186
+ @serializer.load(message)
187
+ end
188
+ end
189
+
170
190
  # Return the query from "storage" (in memory)
171
191
  def read_subscription(subscription_id)
172
192
  query = @subscriptions[subscription_id]
@@ -29,26 +29,10 @@ module GraphQL
29
29
  end
30
30
 
31
31
  # @return [String] an identifier for this unit of subscription
32
- def self.serialize(name, arguments, field, scope:)
33
- normalized_args = case arguments
34
- when GraphQL::Query::Arguments
35
- arguments
36
- when Hash
37
- if field.is_a?(GraphQL::Schema::Field)
38
- stringify_args(field, arguments)
39
- else
40
- GraphQL::Query::LiteralInput.from_arguments(
41
- arguments,
42
- field,
43
- nil,
44
- )
45
- end
46
- else
47
- raise ArgumentError, "Unexpected arguments: #{arguments}, must be Hash or GraphQL::Arguments"
48
- end
49
-
50
- sorted_h = stringify_args(field, normalized_args.to_h)
51
- Serialize.dump_recursive([scope, name, sorted_h])
32
+ def self.serialize(_name, arguments, field, scope:)
33
+ subscription = field.resolver || GraphQL::Schema::Subscription
34
+ normalized_args = stringify_args(field, arguments.to_h)
35
+ subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
52
36
  end
53
37
 
54
38
  # @return [String] a logical identifier for this event. (Stable when the query is broadcastable.)
@@ -6,7 +6,7 @@ module GraphQL
6
6
  description "Represents non-fractional signed whole numeric values. Since the value may exceed the size of a 32-bit integer, it's encoded as a string."
7
7
 
8
8
  def self.coerce_input(value, _ctx)
9
- value && Integer(value)
9
+ value && parse_int(value)
10
10
  rescue ArgumentError
11
11
  nil
12
12
  end
@@ -14,6 +14,10 @@ module GraphQL
14
14
  def self.coerce_result(value, _ctx)
15
15
  value.to_i.to_s
16
16
  end
17
+
18
+ def self.parse_int(value)
19
+ value.is_a?(Numeric) ? value : Integer(value, 10)
20
+ end
17
21
  end
18
22
  end
19
23
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.12.13"
3
+ VERSION = "1.12.17"
4
4
  end
data/lib/graphql.rb CHANGED
@@ -57,22 +57,26 @@ module GraphQL
57
57
  end
58
58
 
59
59
  # Support Ruby 2.2 by implementing `-"str"`. If we drop 2.2 support, we can remove this backport.
60
- module StringDedupBackport
61
- refine String do
62
- def -@
63
- if frozen?
64
- self
65
- else
66
- self.dup.freeze
60
+ if !String.method_defined?(:-@)
61
+ module StringDedupBackport
62
+ refine String do
63
+ def -@
64
+ if frozen?
65
+ self
66
+ else
67
+ self.dup.freeze
68
+ end
67
69
  end
68
70
  end
69
71
  end
70
72
  end
71
73
 
72
- module StringMatchBackport
73
- refine String do
74
- def match?(pattern)
75
- self =~ pattern
74
+ if !String.method_defined?(:match?)
75
+ module StringMatchBackport
76
+ refine String do
77
+ def match?(pattern)
78
+ self =~ pattern
79
+ end
76
80
  end
77
81
  end
78
82
  end
data/readme.md CHANGED
@@ -44,6 +44,6 @@ I also sell [GraphQL::Pro](https://graphql.pro) which provides several features
44
44
 
45
45
  ## Getting Involved
46
46
 
47
- - __Say hi & ask questions__ in the [#ruby channel on Slack](https://graphql-slack.herokuapp.com/) or [on Twitter](https://twitter.com/rmosolgo)!
47
+ - __Say hi & ask questions__ in the #graphql-ruby channel on [Discord](https://discord.com/invite/xud7bH9) or [on Twitter](https://twitter.com/rmosolgo)!
48
48
  - __Report bugs__ by posting a description, full stack trace, and all relevant code in a [GitHub issue](https://github.com/rmosolgo/graphql-ruby/issues).
49
49
  - __Start hacking__ with the [Development guide](https://graphql-ruby.org/development).
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.13
4
+ version: 1.12.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-30 00:00:00.000000000 Z
11
+ date: 2021-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips