graphql 1.12.12 → 1.12.16

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: 296954a3b03f2fd5dae9fae56eaa08ea04b3dfb8f25923201ea27c67147c71fa
4
- data.tar.gz: 4de323637c29b729a3eb3f07b620e2014515b37235c485002d016ead490d86e7
3
+ metadata.gz: 03b30407d3081dad5d25f3a3f9a9e2781cd34db838688816b01c231aaa5a6dc5
4
+ data.tar.gz: a06092995f8e3ea0ed2c75485124b0123d41a23d77eedd773e15a1d00fad80ab
5
5
  SHA512:
6
- metadata.gz: fdf50f6c6f170aa4c50d46356e4089c40a939bf3d0dfd4ab7bdb667d169f01a38320c971cdec1dde32353a20823c1756196312df5e425af41a157caa1179e845
7
- data.tar.gz: 4202ca4741998ee1f5535eea14ff93c1a8529ff5085625d67171e75736f65f55d0433b93512c272e16f4f9a48dd630eba49cf37a54e49b0e9e3dad5f0496930c
6
+ metadata.gz: 3af391b8f7394985a42595af2d7b3735a6666310a4583ffd2a9ddce46cf868a6a8d7f2a9cb28ba6a1b85fb70cce8d7425074df847f8b113420e59599bd0379b5
7
+ data.tar.gz: ca7370709ff588ba437a2f5b277482f2a00543b057ee96e164198ea44f81ad1e470fafe4c7eb89a9e1e64c0aeeddcc128983a67458040c855115516c09adcfd0
@@ -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
@@ -144,7 +144,7 @@ module GraphQL
144
144
  end
145
145
 
146
146
  def value_at(runtime, path)
147
- response = runtime.response
147
+ response = runtime.final_result
148
148
  path.each do |key|
149
149
  if response && (response = response[key])
150
150
  next
@@ -15,14 +15,17 @@ 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
25
- push_key = metadata[:path].reject { |i| i.is_a?(Integer) }
28
+ push_key = metadata[:path]
26
29
  parent_frame = multiplex.context[:graphql_backtrace_contexts][push_key[0..-2]]
27
30
 
28
31
  if parent_frame.is_a?(GraphQL::Query)
@@ -34,7 +34,10 @@ module GraphQL
34
34
  next_results = []
35
35
  while results.any?
36
36
  result_value = results.shift
37
- if result_value.is_a?(Hash)
37
+ if result_value.is_a?(Runtime::GraphQLResultHash) || result_value.is_a?(Hash)
38
+ results.concat(result_value.values)
39
+ next
40
+ elsif result_value.is_a?(Runtime::GraphQLResultArray)
38
41
  results.concat(result_value.values)
39
42
  next
40
43
  elsif result_value.is_a?(Array)
@@ -46,7 +49,8 @@ module GraphQL
46
49
  # Since this field returned another lazy,
47
50
  # add it to the same queue
48
51
  results << loaded_value
49
- elsif loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
52
+ elsif loaded_value.is_a?(Runtime::GraphQLResultHash) || loaded_value.is_a?(Runtime::GraphQLResultArray) ||
53
+ loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
50
54
  # Add these values in wholesale --
51
55
  # they might be modified by later work in the dataloader.
52
56
  next_results << loaded_value
@@ -10,9 +10,19 @@ module GraphQL
10
10
  class Runtime
11
11
 
12
12
  module GraphQLResult
13
- # These methods are private concerns of GraphQL-Ruby,
14
- # they aren't guaranteed to continue working in the future.
15
- 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
+
16
26
  # Although these are used by only one of the Result classes,
17
27
  # it's handy to have the methods implemented on both (even though they just return `nil`)
18
28
  # because it makes it easy to check if anything is assigned.
@@ -20,9 +30,17 @@ module GraphQL
20
30
  attr_accessor :graphql_non_null_field_names
21
31
  # @return [nil, true]
22
32
  attr_accessor :graphql_non_null_list_items
33
+
34
+ # @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects)
35
+ attr_accessor :graphql_result_data
23
36
  end
24
37
 
25
- class GraphQLResultHash < Hash
38
+ class GraphQLResultHash
39
+ def initialize(_result_name, _parent_result)
40
+ super
41
+ @graphql_result_data = {}
42
+ end
43
+
26
44
  include GraphQLResult
27
45
 
28
46
  attr_accessor :graphql_merged_into
@@ -39,12 +57,82 @@ module GraphQL
39
57
  if (t = @graphql_merged_into)
40
58
  t[key] = value
41
59
  end
42
- super
60
+
61
+ if value.respond_to?(:graphql_result_data)
62
+ @graphql_result_data[key] = value.graphql_result_data
63
+ # If we encounter some part of this response that requires metadata tracking,
64
+ # then create the metadata hash if necessary. It will be kept up-to-date after this.
65
+ (@graphql_metadata ||= @graphql_result_data.dup)[key] = value
66
+ else
67
+ @graphql_result_data[key] = value
68
+ # keep this up-to-date if it's been initialized
69
+ @graphql_metadata && @graphql_metadata[key] = value
70
+ end
71
+
72
+ value
73
+ end
74
+
75
+ def delete(key)
76
+ @graphql_metadata && @graphql_metadata.delete(key)
77
+ @graphql_result_data.delete(key)
78
+ end
79
+
80
+ def each
81
+ (@graphql_metadata || @graphql_result_data).each { |k, v| yield(k, v) }
82
+ end
83
+
84
+ def values
85
+ (@graphql_metadata || @graphql_result_data).values
86
+ end
87
+
88
+ def key?(k)
89
+ @graphql_result_data.key?(k)
90
+ end
91
+
92
+ def [](k)
93
+ (@graphql_metadata || @graphql_result_data)[k]
43
94
  end
44
95
  end
45
96
 
46
- class GraphQLResultArray < Array
97
+ class GraphQLResultArray
47
98
  include GraphQLResult
99
+
100
+ def initialize(_result_name, _parent_result)
101
+ super
102
+ @graphql_result_data = []
103
+ end
104
+
105
+ def graphql_skip_at(index)
106
+ # Mark this index as dead. It's tricky because some indices may already be storing
107
+ # `Lazy`s. So the runtime is still holding indexes _before_ skipping,
108
+ # this object has to coordinate incoming writes to account for any already-skipped indices.
109
+ @skip_indices ||= []
110
+ @skip_indices << index
111
+ offset_by = @skip_indices.count { |skipped_idx| skipped_idx < index}
112
+ delete_at_index = index - offset_by
113
+ @graphql_metadata && @graphql_metadata.delete_at(delete_at_index)
114
+ @graphql_result_data.delete_at(delete_at_index)
115
+ end
116
+
117
+ def []=(idx, value)
118
+ if @skip_indices
119
+ offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx }
120
+ idx -= offset_by
121
+ end
122
+ if value.respond_to?(:graphql_result_data)
123
+ @graphql_result_data[idx] = value.graphql_result_data
124
+ (@graphql_metadata ||= @graphql_result_data.dup)[idx] = value
125
+ else
126
+ @graphql_result_data[idx] = value
127
+ @graphql_metadata && @graphql_metadata[idx] = value
128
+ end
129
+
130
+ value
131
+ end
132
+
133
+ def values
134
+ (@graphql_metadata || @graphql_result_data)
135
+ end
48
136
  end
49
137
 
50
138
  class GraphQLSelectionSet < Hash
@@ -60,9 +148,6 @@ module GraphQL
60
148
  # @return [GraphQL::Query::Context]
61
149
  attr_reader :context
62
150
 
63
- # @return [Hash]
64
- attr_reader :response
65
-
66
151
  def initialize(query:)
67
152
  @query = query
68
153
  @dataloader = query.multiplex.dataloader
@@ -70,7 +155,7 @@ module GraphQL
70
155
  @context = query.context
71
156
  @multiplex_context = query.multiplex.context
72
157
  @interpreter_context = @context.namespace(:interpreter)
73
- @response = GraphQLResultHash.new
158
+ @response = GraphQLResultHash.new(nil, nil)
74
159
  # Identify runtime directives by checking which of this schema's directives have overridden `def self.resolve`
75
160
  @runtime_directive_names = []
76
161
  noop_resolve_owner = GraphQL::Schema::Directive.singleton_class
@@ -87,6 +172,10 @@ module GraphQL
87
172
  @lazy_cache = {}
88
173
  end
89
174
 
175
+ def final_result
176
+ @response && @response.graphql_result_data
177
+ end
178
+
90
179
  def inspect
91
180
  "#<#{self.class.name} response=#{@response.inspect}>"
92
181
  end
@@ -128,7 +217,7 @@ module GraphQL
128
217
  # directly evaluated and the results can be written right into the main response hash.
129
218
  tap_or_each(gathered_selections) do |selections, is_selection_array|
130
219
  if is_selection_array
131
- selection_response = GraphQLResultHash.new
220
+ selection_response = GraphQLResultHash.new(nil, nil)
132
221
  final_response = @response
133
222
  else
134
223
  selection_response = @response
@@ -167,7 +256,7 @@ module GraphQL
167
256
  into_result[key] = value
168
257
  else
169
258
  case value
170
- when Hash
259
+ when GraphQLResultHash
171
260
  deep_merge_selection_result(value, into_result[key])
172
261
  else
173
262
  # We have to assume that, since this passed the `fields_will_merge` selection,
@@ -282,6 +371,7 @@ module GraphQL
282
371
 
283
372
  # @return [void]
284
373
  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
+ return if dead_result?(selections_result)
285
375
  # As a performance optimization, the hash key will be a `Node` if
286
376
  # there's only one selection of the field. But if there are multiple
287
377
  # selections of the field, it will be an Array of nodes
@@ -439,15 +529,7 @@ module GraphQL
439
529
  end
440
530
 
441
531
  def dead_result?(selection_result)
442
- r = selection_result
443
- while r
444
- if r.graphql_dead
445
- return true
446
- else
447
- r = r.graphql_parent
448
- end
449
- end
450
- false
532
+ selection_result.graphql_dead || ((parent = selection_result.graphql_parent) && parent.graphql_dead)
451
533
  end
452
534
 
453
535
  def set_result(selection_result, result_name, value)
@@ -471,9 +553,7 @@ module GraphQL
471
553
  @response = nil
472
554
  else
473
555
  set_result(parent, name_in_parent, nil)
474
- # This is odd, but it's how it used to work. Even if `parent` _would_ accept
475
- # a `nil`, it's marked dead. TODO: check the spec, is there a reason for this?
476
- parent.graphql_dead = true
556
+ set_graphql_dead(selection_result)
477
557
  end
478
558
  else
479
559
  selection_result[result_name] = value
@@ -481,6 +561,21 @@ module GraphQL
481
561
  end
482
562
  end
483
563
 
564
+ # Mark this node and any already-registered children as dead,
565
+ # so that it accepts no more writes.
566
+ def set_graphql_dead(selection_result)
567
+ case selection_result
568
+ when GraphQLResultArray
569
+ selection_result.graphql_dead = true
570
+ selection_result.values.each { |v| set_graphql_dead(v) }
571
+ when GraphQLResultHash
572
+ selection_result.graphql_dead = true
573
+ selection_result.each { |k, v| set_graphql_dead(v) }
574
+ else
575
+ # It's a scalar, no way to mark it dead.
576
+ end
577
+ end
578
+
484
579
  HALT = Object.new
485
580
  def continue_value(path, value, parent_type, field, is_non_null, ast_node, result_name, selection_result) # rubocop:disable Metrics/ParameterLists
486
581
  case value
@@ -500,11 +595,13 @@ module GraphQL
500
595
  # to avoid the overhead of checking three different classes
501
596
  # every time.
502
597
  if value.is_a?(GraphQL::ExecutionError)
503
- if !dead_result?(selection_result)
598
+ if selection_result.nil? || !dead_result?(selection_result)
504
599
  value.path ||= path
505
600
  value.ast_node ||= ast_node
506
601
  context.errors << value
507
- set_result(selection_result, result_name, nil)
602
+ if selection_result
603
+ set_result(selection_result, result_name, nil)
604
+ end
508
605
  end
509
606
  HALT
510
607
  elsif value.is_a?(GraphQL::UnauthorizedError)
@@ -517,6 +614,17 @@ module GraphQL
517
614
  end
518
615
  continue_value(path, next_value, parent_type, field, is_non_null, ast_node, result_name, selection_result)
519
616
  elsif GraphQL::Execution::Execute::SKIP == value
617
+ # It's possible a lazy was already written here
618
+ case selection_result
619
+ when GraphQLResultHash
620
+ selection_result.delete(result_name)
621
+ when GraphQLResultArray
622
+ selection_result.graphql_skip_at(result_name)
623
+ when nil
624
+ # this can happen with directives
625
+ else
626
+ raise "Invariant: unexpected result class #{selection_result.class} (#{selection_result.inspect})"
627
+ end
520
628
  HALT
521
629
  else
522
630
  # What could this actually _be_? Anyhow,
@@ -526,13 +634,15 @@ module GraphQL
526
634
  when Array
527
635
  # It's an array full of execution errors; add them all.
528
636
  if value.any? && value.all? { |v| v.is_a?(GraphQL::ExecutionError) }
529
- if !dead_result?(selection_result)
637
+ if selection_result.nil? || !dead_result?(selection_result)
530
638
  value.each_with_index do |error, index|
531
639
  error.ast_node ||= ast_node
532
- error.path ||= path + (field.type.list? ? [index] : [])
640
+ error.path ||= path + ((field && field.type.list?) ? [index] : [])
533
641
  context.errors << error
534
642
  end
535
- set_result(selection_result, result_name, nil)
643
+ if selection_result
644
+ set_result(selection_result, result_name, nil)
645
+ end
536
646
  end
537
647
  HALT
538
648
  else
@@ -593,9 +703,7 @@ module GraphQL
593
703
  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|
594
704
  continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node, result_name, selection_result)
595
705
  if HALT != continue_value
596
- response_hash = GraphQLResultHash.new
597
- response_hash.graphql_parent = selection_result
598
- response_hash.graphql_result_name = result_name
706
+ response_hash = GraphQLResultHash.new(result_name, selection_result)
599
707
  set_result(selection_result, result_name, response_hash)
600
708
  gathered_selections = gather_selections(continue_value, current_type, next_selections)
601
709
  # There are two possibilities for `gathered_selections`:
@@ -608,9 +716,7 @@ module GraphQL
608
716
  # (Technically, it's possible that one of those entries _doesn't_ require isolation.)
609
717
  tap_or_each(gathered_selections) do |selections, is_selection_array|
610
718
  if is_selection_array
611
- this_result = GraphQLResultHash.new
612
- this_result.graphql_parent = selection_result
613
- this_result.graphql_result_name = result_name
719
+ this_result = GraphQLResultHash.new(result_name, selection_result)
614
720
  final_result = response_hash
615
721
  else
616
722
  this_result = response_hash
@@ -635,16 +741,15 @@ module GraphQL
635
741
  end
636
742
  when "LIST"
637
743
  inner_type = current_type.of_type
638
- response_list = GraphQLResultArray.new
744
+ response_list = GraphQLResultArray.new(result_name, selection_result)
639
745
  response_list.graphql_non_null_list_items = inner_type.non_null?
640
- response_list.graphql_parent = selection_result
641
- response_list.graphql_result_name = result_name
642
746
  set_result(selection_result, result_name, response_list)
643
747
 
644
748
  idx = 0
645
749
  scoped_context = context.scoped_context
646
750
  begin
647
751
  value.each do |inner_value|
752
+ break if dead_result?(response_list)
648
753
  next_path = path.dup
649
754
  next_path << idx
650
755
  this_idx = idx
@@ -689,9 +794,24 @@ module GraphQL
689
794
  if !dir_defn.is_a?(Class)
690
795
  dir_defn = dir_defn.type_class || raise("Only class-based directives are supported (not `@#{dir_node.name}`)")
691
796
  end
692
- dir_args = arguments(nil, dir_defn, dir_node)
693
- dir_defn.resolve(object, dir_args, context) do
694
- run_directive(object, directives, idx + 1, &block)
797
+ raw_dir_args = arguments(nil, dir_defn, dir_node)
798
+ dir_args = continue_value(
799
+ @context[:current_path], # path
800
+ raw_dir_args, # value
801
+ dir_defn, # parent_type
802
+ nil, # field
803
+ false, # is_non_null
804
+ dir_node, # ast_node
805
+ nil, # result_name
806
+ nil, # selection_result
807
+ )
808
+
809
+ if dir_args == HALT
810
+ nil
811
+ else
812
+ dir_defn.resolve(object, dir_args, context) do
813
+ run_directive(object, directives, idx + 1, &block)
814
+ end
695
815
  end
696
816
  end
697
817
  end
@@ -18,7 +18,7 @@ module GraphQL
18
18
  def execute(_operation, _root_type, query)
19
19
  runtime = evaluate(query)
20
20
  sync_lazies(query: query)
21
- runtime.response
21
+ runtime.final_result
22
22
  end
23
23
 
24
24
  def self.use(schema_class)
@@ -56,7 +56,7 @@ module GraphQL
56
56
 
57
57
  def self.finish_query(query, _multiplex)
58
58
  {
59
- "data" => query.context.namespace(:interpreter)[:runtime].response
59
+ "data" => query.context.namespace(:interpreter)[:runtime].final_result
60
60
  }
61
61
  end
62
62
 
@@ -87,7 +87,7 @@ module GraphQL
87
87
  final_values = queries.map do |query|
88
88
  runtime = query.context.namespace(:interpreter)[:runtime]
89
89
  # it might not be present if the query has an error
90
- runtime ? runtime.response : nil
90
+ runtime ? runtime.final_result : nil
91
91
  end
92
92
  final_values.compact!
93
93
  tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
@@ -48,7 +48,11 @@ module GraphQL
48
48
  end
49
49
  end
50
50
 
51
- if @value.is_a?(StandardError)
51
+ # `SKIP` was made into a subclass of `GraphQL::Error` to improve runtime performance
52
+ # (fewer clauses in a hot `case` block), but now it requires special handling here.
53
+ # I think it's still worth it for the performance win, but if the number of special
54
+ # cases grows, then maybe it's worth rethinking somehow.
55
+ if @value.is_a?(StandardError) && @value != GraphQL::Execution::Execute::SKIP
52
56
  raise @value
53
57
  else
54
58
  @value
@@ -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
@@ -270,7 +270,7 @@ module GraphQL
270
270
  # @return [String, nil] Returns nil if the query is invalid.
271
271
  def sanitized_query_string(inline_variables: true)
272
272
  with_prepared_ast {
273
- GraphQL::Language::SanitizedPrinter.new(self, inline_variables: inline_variables).sanitized_query_string
273
+ schema.sanitized_printer.new(self, inline_variables: inline_variables).sanitized_query_string
274
274
  }
275
275
  end
276
276
 
@@ -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
 
@@ -39,7 +39,7 @@ module GraphQL
39
39
  transform_name = arguments[:by]
40
40
  if TRANSFORMS.include?(transform_name) && return_value.respond_to?(transform_name)
41
41
  return_value = return_value.public_send(transform_name)
42
- response = context.namespace(:interpreter)[:runtime].response
42
+ response = context.namespace(:interpreter)[:runtime].final_result
43
43
  *keys, last = path
44
44
  keys.each do |key|
45
45
  if response && (response = response[key])
@@ -24,6 +24,15 @@ module GraphQL
24
24
  extend GraphQL::Schema::Member::ValidatesInput
25
25
 
26
26
  class UnresolvedValueError < GraphQL::EnumType::UnresolvedValueError
27
+ def initialize(value:, enum:, context:)
28
+ fix_message = ", but this isn't a valid value for `#{enum.graphql_name}`. Update the field or resolver to return one of `#{enum.graphql_name}`'s values instead."
29
+ message = if (cp = context[:current_path]) && (cf = context[:current_field])
30
+ "`#{cf.path}` returned `#{value.inspect}` at `#{cp.join(".")}`#{fix_message}"
31
+ else
32
+ "`#{value.inspect}` was returned for `#{enum.graphql_name}`#{fix_message}"
33
+ end
34
+ super(message)
35
+ end
27
36
  end
28
37
 
29
38
  class << self
@@ -100,7 +109,7 @@ module GraphQL
100
109
  if enum_value
101
110
  enum_value.graphql_name
102
111
  else
103
- raise(self::UnresolvedValueError, "Can't resolve enum #{graphql_name} for #{value.inspect}")
112
+ raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx)
104
113
  end
105
114
  end
106
115
 
@@ -11,6 +11,14 @@ module GraphQL
11
11
 
12
12
  include GraphQL::Dig
13
13
 
14
+ # @return [GraphQL::Query::Context] The context for this query
15
+ attr_reader :context
16
+ # @return [GraphQL::Query::Arguments, GraphQL::Execution::Interpereter::Arguments] The underlying arguments instance
17
+ attr_reader :arguments
18
+
19
+ # Ruby-like hash behaviors, read-only
20
+ def_delegators :@ruby_style_hash, :keys, :values, :each, :map, :any?, :empty?
21
+
14
22
  def initialize(arguments = nil, ruby_kwargs: nil, context:, defaults_used:)
15
23
  @context = context
16
24
  if ruby_kwargs
@@ -54,19 +62,8 @@ module GraphQL
54
62
  @maybe_lazies = maybe_lazies
55
63
  end
56
64
 
57
- # @return [GraphQL::Query::Context] The context for this query
58
- attr_reader :context
59
-
60
- # @return [GraphQL::Query::Arguments, GraphQL::Execution::Interpereter::Arguments] The underlying arguments instance
61
- attr_reader :arguments
62
-
63
- # Ruby-like hash behaviors, read-only
64
- def_delegators :@ruby_style_hash, :keys, :values, :each, :map, :any?, :empty?
65
-
66
65
  def to_h
67
- @ruby_style_hash.inject({}) do |h, (key, value)|
68
- h.merge(key => unwrap_value(value))
69
- end
66
+ unwrap_value(@ruby_style_hash)
70
67
  end
71
68
 
72
69
  def to_hash
@@ -91,8 +88,8 @@ module GraphQL
91
88
  when Array
92
89
  value.map { |item| unwrap_value(item) }
93
90
  when Hash
94
- value.inject({}) do |h, (key, value)|
95
- h.merge(key => unwrap_value(value))
91
+ value.reduce({}) do |h, (key, value)|
92
+ h.merge!(key => unwrap_value(value))
96
93
  end
97
94
  when InputObject
98
95
  value.to_h
@@ -162,7 +159,6 @@ module GraphQL
162
159
  # @api private
163
160
  INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object responding to `to_h` or `to_unsafe_h`."
164
161
 
165
-
166
162
  def validate_non_null_input(input, ctx)
167
163
  result = GraphQL::Query::InputValidationResult.new
168
164
 
@@ -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
@@ -4,37 +4,32 @@ module GraphQL
4
4
  # Used to convert your {GraphQL::Schema} to a GraphQL schema string
5
5
  #
6
6
  # @example print your schema to standard output (via helper)
7
- # MySchema = GraphQL::Schema.define(query: QueryType)
8
7
  # puts GraphQL::Schema::Printer.print_schema(MySchema)
9
8
  #
10
9
  # @example print your schema to standard output
11
- # MySchema = GraphQL::Schema.define(query: QueryType)
12
10
  # puts GraphQL::Schema::Printer.new(MySchema).print_schema
13
11
  #
14
12
  # @example print a single type to standard output
15
- # query_root = GraphQL::ObjectType.define do
16
- # name "Query"
13
+ # class Types::Query < GraphQL::Schema::Object
17
14
  # description "The query root of this schema"
18
15
  #
19
- # field :post do
20
- # type post_type
21
- # resolve ->(obj, args, ctx) { Post.find(args["id"]) }
22
- # end
16
+ # field :post, Types::Post, null: true
23
17
  # end
24
18
  #
25
- # post_type = GraphQL::ObjectType.define do
26
- # name "Post"
19
+ # class Types::Post < GraphQL::Schema::Object
27
20
  # description "A blog post"
28
21
  #
29
- # field :id, !types.ID
30
- # field :title, !types.String
31
- # field :body, !types.String
22
+ # field :id, ID, null: false
23
+ # field :title, String, null: false
24
+ # field :body, String, null: false
32
25
  # end
33
26
  #
34
- # MySchema = GraphQL::Schema.define(query: query_root)
27
+ # class MySchema < GraphQL::Schema
28
+ # query(Types::Query)
29
+ # end
35
30
  #
36
31
  # printer = GraphQL::Schema::Printer.new(MySchema)
37
- # puts printer.print_type(post_type)
32
+ # puts printer.print_type(Types::Post)
38
33
  #
39
34
  class Printer < GraphQL::Language::Printer
40
35
  attr_reader :schema, :warden
@@ -87,7 +82,7 @@ module GraphQL
87
82
 
88
83
  # Return a GraphQL schema string for the defined types in the schema
89
84
  def print_schema
90
- print(@document)
85
+ print(@document) + "\n"
91
86
  end
92
87
 
93
88
  def print_type(type)
@@ -307,10 +307,15 @@ module GraphQL
307
307
  arguments: arguments,
308
308
  null: null,
309
309
  complexity: complexity,
310
- extensions: extensions,
311
310
  broadcastable: broadcastable?,
312
311
  }
313
312
 
313
+ # If there aren't any, then the returned array is `[].freeze`,
314
+ # but passing that along breaks some user code.
315
+ if (exts = extensions).any?
316
+ field_opts[:extensions] = exts
317
+ end
318
+
314
319
  if has_max_page_size?
315
320
  field_opts[:max_page_size] = max_page_size
316
321
  end
@@ -116,6 +116,26 @@ module GraphQL
116
116
  end
117
117
  end
118
118
 
119
+ # This is called during initial subscription to get a "name" for this subscription.
120
+ # Later, when `.trigger` is called, this will be called again to build another "name".
121
+ # Any subscribers with matching topic will begin the update flow.
122
+ #
123
+ # The default implementation creates a string using the field name, subscription scope, and argument keys and values.
124
+ # In that implementation, only `.trigger` calls with _exact matches_ result in updates to subscribers.
125
+ #
126
+ # To implement a filtered stream-type subscription flow, override this method to return a string with field name and subscription scope.
127
+ # Then, implement {#update} to compare its arguments to the current `object` and return `:no_update` when an
128
+ # update should be filtered out.
129
+ #
130
+ # @see {#update} for how to skip updates when an event comes with a matching topic.
131
+ # @param arguments [Hash<String => Object>] The arguments for this topic, in GraphQL-style (camelized strings)
132
+ # @param field [GraphQL::Schema::Field]
133
+ # @param scope [Object, nil] A value corresponding to `.trigger(... scope:)` (for updates) or the `subscription_scope` found in `context` (for initial subscriptions).
134
+ # @return [String] An identifier corresponding to a stream of updates
135
+ def self.topic_for(arguments:, field:, scope:)
136
+ Subscriptions::Serialize.dump_recursive([scope, field.graphql_name, arguments])
137
+ end
138
+
119
139
  # Overriding Resolver#field_options to include subscription_scope
120
140
  def self.field_options
121
141
  super.merge(
@@ -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
@@ -3,6 +3,7 @@
3
3
  module GraphQL
4
4
  module Types
5
5
  module Relay
6
+ # Include this module to your root Query type to get a Relay-compliant `node(id: ID!): Node` field that uses the schema's `object_from_id` hook.
6
7
  module HasNodeField
7
8
  def self.included(child_class)
8
9
  child_class.field(**field_options, &field_block)
@@ -12,7 +13,6 @@ module GraphQL
12
13
  def field_options
13
14
  {
14
15
  name: "node",
15
- owner: nil,
16
16
  type: GraphQL::Types::Relay::Node,
17
17
  null: true,
18
18
  description: "Fetches an object given its ID.",
@@ -3,6 +3,7 @@
3
3
  module GraphQL
4
4
  module Types
5
5
  module Relay
6
+ # Include this module to your root Query type to get a Relay-style `nodes(id: ID!): [Node]` field that uses the schema's `object_from_id` hook.
6
7
  module HasNodesField
7
8
  def self.included(child_class)
8
9
  child_class.field(**field_options, &field_block)
@@ -12,7 +13,6 @@ module GraphQL
12
13
  def field_options
13
14
  {
14
15
  name: "nodes",
15
- owner: nil,
16
16
  type: [GraphQL::Types::Relay::Node, null: true],
17
17
  null: false,
18
18
  description: "Fetches a list of objects given a list of IDs.",
@@ -6,7 +6,7 @@ module GraphQL
6
6
  # or use it for inspiration for your own field definition.
7
7
  #
8
8
  # @example Adding this field directly
9
- # add_field(GraphQL::Types::Relay::NodeField)
9
+ # include GraphQL::Types::Relay::HasNodeField
10
10
  #
11
11
  # @example Implementing a similar field in your own Query root
12
12
  #
@@ -19,7 +19,7 @@ module GraphQL
19
19
  # context.schema.object_from_id(id, context)
20
20
  # end
21
21
  #
22
- NodeField = GraphQL::Schema::Field.new(**HasNodeField.field_options, &HasNodeField.field_block)
22
+ NodeField = GraphQL::Schema::Field.new(owner: nil, **HasNodeField.field_options, &HasNodeField.field_block)
23
23
  end
24
24
  end
25
25
  end
@@ -6,7 +6,7 @@ module GraphQL
6
6
  # or use it for inspiration for your own field definition.
7
7
  #
8
8
  # @example Adding this field directly
9
- # add_field(GraphQL::Types::Relay::NodesField)
9
+ # include GraphQL::Types::Relay::HasNodesField
10
10
  #
11
11
  # @example Implementing a similar field in your own Query root
12
12
  #
@@ -21,7 +21,7 @@ module GraphQL
21
21
  # end
22
22
  # end
23
23
  #
24
- NodesField = GraphQL::Schema::Field.new(**HasNodesField.field_options, &HasNodesField.field_block)
24
+ NodesField = GraphQL::Schema::Field.new(owner: nil, **HasNodesField.field_options, &HasNodesField.field_block)
25
25
  end
26
26
  end
27
27
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.12.12"
3
+ VERSION = "1.12.16"
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
@@ -2,9 +2,6 @@
2
2
 
3
3
  [![CI Suite](https://github.com/rmosolgo/graphql-ruby/actions/workflows/ci.yaml/badge.svg)](https://github.com/rmosolgo/graphql-ruby/actions/workflows/ci.yaml)
4
4
  [![Gem Version](https://badge.fury.io/rb/graphql.svg)](https://rubygems.org/gems/graphql)
5
- [![Code Climate](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/gpa.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
6
- [![Test Coverage](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/coverage.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
7
- [![built with love](https://cloud.githubusercontent.com/assets/2231765/6766607/d07992c6-cfc9-11e4-813f-d9240714dd50.png)](https://rmosolgo.github.io/react-badges/)
8
5
 
9
6
  A Ruby implementation of [GraphQL](https://graphql.org/).
10
7
 
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.12
4
+ version: 1.12.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-31 00:00:00.000000000 Z
11
+ date: 2021-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: codeclimate-test-reporter
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '0.4'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '0.4'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: concurrent-ruby
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -150,20 +136,6 @@ dependencies:
150
136
  - - '='
151
137
  - !ruby/object:Gem::Version
152
138
  version: '0.68'
153
- - !ruby/object:Gem::Dependency
154
- name: stackprof
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
139
  - !ruby/object:Gem::Dependency
168
140
  name: parser
169
141
  requirement: !ruby/object:Gem::Requirement
@@ -713,7 +685,7 @@ metadata:
713
685
  source_code_uri: https://github.com/rmosolgo/graphql-ruby
714
686
  bug_tracker_uri: https://github.com/rmosolgo/graphql-ruby/issues
715
687
  mailing_list_uri: https://tinyletter.com/graphql-ruby
716
- post_install_message:
688
+ post_install_message:
717
689
  rdoc_options: []
718
690
  require_paths:
719
691
  - lib
@@ -728,8 +700,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
728
700
  - !ruby/object:Gem::Version
729
701
  version: '0'
730
702
  requirements: []
731
- rubygems_version: 3.1.4
732
- signing_key:
703
+ rubygems_version: 3.2.15
704
+ signing_key:
733
705
  specification_version: 4
734
706
  summary: A GraphQL language and runtime for Ruby
735
707
  test_files: []