graphql 1.12.12 → 1.12.16

Sign up to get free protection for your applications and to get access to all the features.
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: []