graphql 1.11.1 → 1.11.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b152fd76301d994c8a685bce0539e409739c6e1e303c611c34a6424b2d2e68e
4
- data.tar.gz: 84215fce3790161bebfa183e05b69abe6394396bec177ce69f98e5a53484d7b5
3
+ metadata.gz: bafdf456b47766daa370e9f8559e2f525b37d24b6f5a253742eba71759428f29
4
+ data.tar.gz: f2dc55cb9c2777104f23358a27e01328ed4b4df5334ce237e9af1cae731c3ab7
5
5
  SHA512:
6
- metadata.gz: 83d130c2cd4c76f9869d60e4a32f80e2a20a3ff5c614e6852686c1d06d2da8b454065b5c382099877c2449e77f938f8ef5e2dfd69e22bf5a505e04a3edb8db2d
7
- data.tar.gz: 06bf9038ca1f32d03fc4a0fc2ed6763420ff58d7c7b4aaad38a703b0eb090c3a35fb829c2a7e13420c1507ea8013e5d7084067c733de98b416698ea39ba46ba3
6
+ metadata.gz: f7120485f538e60577a55b9674cb21e89785d10becfb5545e8412c284547b4c066c3c7abff15b53edca990cb49fd659eeb0df16e84f6bfb511aca89dd41d2834
7
+ data.tar.gz: a48a2c59655e72c44d711f69cd94c7649a0ada18e1dba50cabadaa8a2748b12021ed7f03a6accca78bfba4aab93de2d37cf0b5e119f3f08a12a7d9784667af15
@@ -5,7 +5,7 @@ class GraphqlController < ApplicationController
5
5
  # protect_from_forgery with: :null_session
6
6
 
7
7
  def execute
8
- variables = ensure_hash(params[:variables])
8
+ variables = prepare_variables(params[:variables])
9
9
  query = params[:query]
10
10
  operation_name = params[:operationName]
11
11
  context = {
@@ -21,21 +21,23 @@ class GraphqlController < ApplicationController
21
21
 
22
22
  private
23
23
 
24
- # Handle form data, JSON body, or a blank value
25
- def ensure_hash(ambiguous_param)
26
- case ambiguous_param
24
+ # Handle variables in form data, JSON body, or a blank value
25
+ def prepare_variables(variables_param)
26
+ case variables_param
27
27
  when String
28
- if ambiguous_param.present?
29
- ensure_hash(JSON.parse(ambiguous_param))
28
+ if variables_param.present?
29
+ JSON.parse(variables_param) || {}
30
30
  else
31
31
  {}
32
32
  end
33
- when Hash, ActionController::Parameters
34
- ambiguous_param
33
+ when Hash
34
+ variables_param
35
+ when ActionController::Parameters
36
+ variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
35
37
  when nil
36
38
  {}
37
39
  else
38
- raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
40
+ raise ArgumentError, "Unexpected parameter: #{variables_param}"
39
41
  end
40
42
  end
41
43
 
@@ -99,6 +99,10 @@ module GraphQL
99
99
  def type_class
100
100
  metadata[:type_class]
101
101
  end
102
+
103
+ def get_argument(argument_name)
104
+ arguments[argument_name]
105
+ end
102
106
  end
103
107
  end
104
108
 
@@ -461,8 +461,9 @@ module GraphQL
461
461
  end
462
462
 
463
463
  def arguments(graphql_object, arg_owner, ast_node)
464
- # Don't cache arguments if field extras are requested since extras mutate the argument data structure
465
- if arg_owner.arguments_statically_coercible? && (!arg_owner.is_a?(GraphQL::Schema::Field) || arg_owner.extras.empty?)
464
+ # Don't cache arguments if field extras or extensions are requested since they can mutate the argument data structure
465
+ if arg_owner.arguments_statically_coercible? &&
466
+ (!arg_owner.is_a?(GraphQL::Schema::Field) || (arg_owner.extras.empty? && arg_owner.extensions.empty?))
466
467
  query.arguments_for(ast_node, arg_owner)
467
468
  else
468
469
  # The arguments must be prepared in the context of the given object
@@ -207,6 +207,10 @@ module GraphQL
207
207
  metadata[:type_class]
208
208
  end
209
209
 
210
+ def get_argument(argument_name)
211
+ arguments[argument_name]
212
+ end
213
+
210
214
  private
211
215
 
212
216
  def build_default_resolver
@@ -58,6 +58,10 @@ module GraphQL
58
58
  result
59
59
  end
60
60
 
61
+ def get_argument(argument_name)
62
+ arguments[argument_name]
63
+ end
64
+
61
65
  private
62
66
 
63
67
  def coerce_non_null_input(value, ctx)
@@ -524,6 +524,7 @@ module GraphQL
524
524
 
525
525
  # Usage of a variable in a query. Name does _not_ include `$`.
526
526
  class VariableIdentifier < NameOnlyNode
527
+ self.children_method_name = :value
527
528
  end
528
529
 
529
530
  class SchemaDefinition < AbstractNode
@@ -89,7 +89,7 @@ module GraphQL
89
89
  # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
90
90
  # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`.
91
91
  def on_abstract_node(node, parent)
92
- if node == DELETE_NODE
92
+ if node.equal?(DELETE_NODE)
93
93
  # This might be passed to `super(DELETE_NODE, ...)`
94
94
  # by a user hook, don't want to keep visiting in that case.
95
95
  nil
@@ -179,7 +179,7 @@ module GraphQL
179
179
  # The user-provided hook returned a new node.
180
180
  new_parent = new_parent && new_parent.replace_child(node, new_node)
181
181
  return new_node, new_parent
182
- elsif new_node == DELETE_NODE
182
+ elsif new_node.equal?(DELETE_NODE)
183
183
  # The user-provided hook requested to remove this node
184
184
  new_parent = new_parent && new_parent.delete_child(node)
185
185
  return nil, new_parent
@@ -15,11 +15,6 @@ module GraphQL
15
15
  class PaginationImplementationMissingError < GraphQL::Error
16
16
  end
17
17
 
18
- # @return [Class] The class to use for wrapping items as `edges { ... }`. Defaults to `Connection::Edge`
19
- def self.edge_class
20
- self::Edge
21
- end
22
-
23
18
  # @return [Object] A list object, from the application. This is the unpaginated value passed into the connection.
24
19
  attr_reader :items
25
20
 
@@ -58,7 +53,7 @@ module GraphQL
58
53
  # @param last [Integer, nil] Limit parameter from the client, if provided
59
54
  # @param before [String, nil] A cursor for pagination, if the client provided one.
60
55
  # @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given.
61
- def initialize(items, parent: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil)
56
+ def initialize(items, parent: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil, edge_class: nil)
62
57
  @items = items
63
58
  @parent = parent
64
59
  @context = context
@@ -66,7 +61,7 @@ module GraphQL
66
61
  @after_value = after
67
62
  @last_value = last
68
63
  @before_value = before
69
-
64
+ @edge_class = edge_class || self.class::Edge
70
65
  # This is only true if the object was _initialized_ with an override
71
66
  # or if one is assigned later.
72
67
  @has_max_page_size_override = max_page_size != :not_given
@@ -117,9 +112,12 @@ module GraphQL
117
112
 
118
113
  # @return [Array<Edge>] {nodes}, but wrapped with Edge instances
119
114
  def edges
120
- @edges ||= nodes.map { |n| self.class.edge_class.new(n, self) }
115
+ @edges ||= nodes.map { |n| @edge_class.new(n, self) }
121
116
  end
122
117
 
118
+ # @return [Class] A wrapper class for edges of this connection
119
+ attr_accessor :edge_class
120
+
123
121
  # @return [Array<Object>] A slice of {items}, constrained by {@first_value}/{@after_value}/{@last_value}/{@before_value}
124
122
  def nodes
125
123
  raise PaginationImplementationMissingError, "Implement #{self.class}#nodes to paginate `@items`"
@@ -86,9 +86,21 @@ module GraphQL
86
86
  after: arguments[:after],
87
87
  last: arguments[:last],
88
88
  before: arguments[:before],
89
+ edge_class: edge_class_for_field(field),
89
90
  )
90
91
  end
91
92
 
93
+ # use an override if there is one
94
+ # @api private
95
+ def edge_class_for_field(field)
96
+ conn_type = field.type.unwrap
97
+ conn_type_edge_type = conn_type.respond_to?(:edge_class) && conn_type.edge_class
98
+ if conn_type_edge_type && conn_type_edge_type != Relay::Edge
99
+ conn_type_edge_type
100
+ else
101
+ nil
102
+ end
103
+ end
92
104
  protected
93
105
 
94
106
  attr_reader :wrappers
@@ -127,7 +127,7 @@ module GraphQL
127
127
  end
128
128
  end
129
129
 
130
- # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve}.
130
+ # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {#lazy_resolve}.
131
131
  def lazy_method_name(obj)
132
132
  lazy_methods.get(obj)
133
133
  end
@@ -1534,9 +1534,9 @@ module GraphQL
1534
1534
 
1535
1535
  # Add several directives at once
1536
1536
  # @param new_directives [Class]
1537
- def directives(new_directives = nil)
1538
- if new_directives
1539
- new_directives.each { |d| directive(d) }
1537
+ def directives(*new_directives)
1538
+ if new_directives.any?
1539
+ new_directives.flatten.each { |d| directive(d) }
1540
1540
  end
1541
1541
 
1542
1542
  find_inherited_value(:directives, default_directives).merge(own_directives)
@@ -1550,11 +1550,11 @@ module GraphQL
1550
1550
  end
1551
1551
 
1552
1552
  def default_directives
1553
- {
1553
+ @default_directives ||= {
1554
1554
  "include" => GraphQL::Schema::Directive::Include,
1555
1555
  "skip" => GraphQL::Schema::Directive::Skip,
1556
1556
  "deprecated" => GraphQL::Schema::Directive::Deprecated,
1557
- }
1557
+ }.freeze
1558
1558
  end
1559
1559
 
1560
1560
  def tracer(new_tracer)
@@ -1783,16 +1783,7 @@ module GraphQL
1783
1783
  if owner.kind.union?
1784
1784
  # It's a union with possible_types
1785
1785
  # Replace the item by class name
1786
- owner.type_memberships.each { |tm|
1787
- possible_type = tm.object_type
1788
- if possible_type.is_a?(String) && (possible_type == type.name)
1789
- # This is a match of Ruby class names, not graphql names,
1790
- # since strings are used to refer to constants.
1791
- tm.object_type = type
1792
- elsif possible_type.is_a?(LateBoundType) && possible_type.graphql_name == type.graphql_name
1793
- tm.object_type = type
1794
- end
1795
- }
1786
+ owner.assign_type_membership_object_type(type)
1796
1787
  own_possible_types[owner.graphql_name] = owner.possible_types
1797
1788
  elsif type.kind.interface? && owner.kind.object?
1798
1789
  new_interfaces = []
@@ -61,6 +61,11 @@ module GraphQL
61
61
  @from_resolver = from_resolver
62
62
  @method_access = method_access
63
63
 
64
+ if !@null && default_value?
65
+ raise ArgumentError, "Argument '#{@name}' has conflicting params, " \
66
+ "either use `required: false` or remove `default_value:`."
67
+ end
68
+
64
69
  if definition_block
65
70
  if definition_block.arity == 1
66
71
  instance_exec(self, &definition_block)
@@ -41,6 +41,7 @@ module GraphQL
41
41
 
42
42
  def initialize(graphql_name, desc = nil, owner:, ast_node: nil, description: nil, value: nil, deprecation_reason: nil, &block)
43
43
  @graphql_name = graphql_name.to_s
44
+ GraphQL::NameValidator.validate!(@graphql_name)
44
45
  @description = desc || description
45
46
  @value = value.nil? ? @graphql_name : value
46
47
  @deprecation_reason = deprecation_reason
@@ -39,6 +39,9 @@ module GraphQL
39
39
  if field.has_max_page_size? && !value.has_max_page_size_override?
40
40
  value.max_page_size = field.max_page_size
41
41
  end
42
+ if (custom_t = context.schema.connections.edge_class_for_field(@field))
43
+ value.edge_class = custom_t
44
+ end
42
45
  value
43
46
  elsif context.schema.new_connections?
44
47
  wrappers = context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
@@ -166,10 +166,7 @@ module GraphQL
166
166
  return result
167
167
  end
168
168
 
169
- # We're not actually _using_ the coerced result, we're just
170
- # using these methods to make sure that the object will
171
- # behave like a hash below, when we call `each` on it.
172
- begin
169
+ input = begin
173
170
  input.to_h
174
171
  rescue
175
172
  begin
@@ -182,21 +179,25 @@ module GraphQL
182
179
  end
183
180
  end
184
181
 
185
- visible_arguments_map = warden.arguments(self).reduce({}) { |m, f| m[f.name] = f; m}
186
-
187
- # Items in the input that are unexpected
188
- input.each do |name, value|
189
- if visible_arguments_map[name].nil?
190
- result.add_problem("Field is not defined on #{self.graphql_name}", [name])
182
+ # Inject missing required arguments
183
+ missing_required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)|
184
+ if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
185
+ m[argument_name] = nil
191
186
  end
187
+
188
+ m
192
189
  end
193
190
 
194
- # Items in the input that are expected, but have invalid values
195
- visible_arguments_map.map do |name, argument|
196
- argument_result = argument.type.validate_input(input[name], ctx)
197
- if !argument_result.valid?
198
- result.merge_result!(name, argument_result)
191
+ input.merge(missing_required_inputs).each do |argument_name, value|
192
+ argument = warden.get_argument(self, argument_name)
193
+ # Items in the input that are unexpected
194
+ unless argument
195
+ result.add_problem("Field is not defined on #{self.graphql_name}", [argument_name])
196
+ next
199
197
  end
198
+ # Items in the input that are expected, but have invalid values
199
+ argument_result = argument.type.validate_input(value, ctx)
200
+ result.merge_result!(argument_name, argument_result) unless argument_result.valid?
200
201
  end
201
202
 
202
203
  result
@@ -25,8 +25,15 @@ module GraphQL
25
25
  types[type["name"]] = type_object
26
26
  end
27
27
 
28
+ directives = []
29
+ schema.fetch("directives", []).each do |directive|
30
+ next if GraphQL::Schema.default_directives.include?(directive.fetch("name"))
31
+ directives << define_directive(directive, type_resolver)
32
+ end
33
+
28
34
  Class.new(GraphQL::Schema) do
29
35
  orphan_types(types.values)
36
+ directives(directives)
30
37
 
31
38
  def self.resolve_type(*)
32
39
  raise(GraphQL::RequiredImplementationMissingError, "This schema was loaded from string, so it can't resolve types for objects")
@@ -98,7 +105,7 @@ module GraphQL
98
105
  value(
99
106
  enum_value["name"],
100
107
  description: enum_value["description"],
101
- deprecation_reason: enum_value["deprecation_reason"],
108
+ deprecation_reason: enum_value["deprecationReason"],
102
109
  )
103
110
  end
104
111
  end
@@ -147,6 +154,16 @@ module GraphQL
147
154
  end
148
155
  end
149
156
 
157
+ def define_directive(directive, type_resolver)
158
+ loader = self
159
+ Class.new(GraphQL::Schema::Directive) do
160
+ graphql_name(directive["name"])
161
+ description(directive["description"])
162
+ locations(*directive["locations"].map(&:to_sym))
163
+ loader.build_arguments(self, directive["args"], type_resolver)
164
+ end
165
+ end
166
+
150
167
  public
151
168
 
152
169
  def build_fields(type_defn, fields, type_resolver)
@@ -156,6 +173,7 @@ module GraphQL
156
173
  field_hash["name"],
157
174
  type: type_resolver.call(field_hash["type"]),
158
175
  description: field_hash["description"],
176
+ deprecation_reason: field_hash["deprecationReason"],
159
177
  null: true,
160
178
  camelize: false,
161
179
  ) do
@@ -58,6 +58,22 @@ module GraphQL
58
58
  end
59
59
  end
60
60
 
61
+ # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name.
62
+ def get_argument(argument_name)
63
+ a = own_arguments[argument_name]
64
+
65
+ if a || !self.is_a?(Class)
66
+ a
67
+ else
68
+ for ancestor in ancestors
69
+ if ancestor.respond_to?(:own_arguments) && a = ancestor.own_arguments[argument_name]
70
+ return a
71
+ end
72
+ end
73
+ nil
74
+ end
75
+ end
76
+
61
77
  # @param new_arg_class [Class] A class to use for building argument definitions
62
78
  def argument_class(new_arg_class = nil)
63
79
  self.class.argument_class(new_arg_class)
@@ -14,6 +14,7 @@ module GraphQL
14
14
  def possible_types(*types, context: GraphQL::Query::NullContext, **options)
15
15
  if types.any?
16
16
  types.each do |t|
17
+ assert_valid_union_member(t)
17
18
  type_memberships << type_membership_class.new(self, t, **options)
18
19
  end
19
20
  else
@@ -55,6 +56,34 @@ module GraphQL
55
56
  def type_memberships
56
57
  @type_memberships ||= []
57
58
  end
59
+
60
+ # Update a type membership whose `.object_type` is a string or late-bound type
61
+ # so that the type membership's `.object_type` is the given `object_type`.
62
+ # (This is used for updating the union after the schema as lazily loaded the union member.)
63
+ # @api private
64
+ def assign_type_membership_object_type(object_type)
65
+ assert_valid_union_member(object_type)
66
+ type_memberships.each { |tm|
67
+ possible_type = tm.object_type
68
+ if possible_type.is_a?(String) && (possible_type == object_type.name)
69
+ # This is a match of Ruby class names, not graphql names,
70
+ # since strings are used to refer to constants.
71
+ tm.object_type = object_type
72
+ elsif possible_type.is_a?(LateBoundType) && possible_type.graphql_name == object_type.graphql_name
73
+ tm.object_type = object_type
74
+ end
75
+ }
76
+ nil
77
+ end
78
+
79
+ private
80
+
81
+ def assert_valid_union_member(type_defn)
82
+ if type_defn.is_a?(Module) && !type_defn.is_a?(Class)
83
+ # it's an interface type, defined as a module
84
+ raise ArgumentError, "Union possible_types can only be object types (not interface types), remove #{type_defn.graphql_name} (#{type_defn.inspect})"
85
+ end
86
+ end
58
87
  end
59
88
  end
60
89
  end
@@ -105,6 +105,12 @@ module GraphQL
105
105
  @visible_parent_fields[parent_type][field_name]
106
106
  end
107
107
 
108
+ # @return [GraphQL::Argument, nil] The argument named `argument_name` on `parent_type`, if it exists and is visible
109
+ def get_argument(parent_type, argument_name)
110
+ argument = parent_type.get_argument(argument_name)
111
+ return argument if argument && visible_argument?(argument)
112
+ end
113
+
108
114
  # @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
109
115
  def possible_types(type_defn)
110
116
  @visible_possible_types ||= read_through { |type_defn|
@@ -95,16 +95,17 @@ module GraphQL
95
95
  def required_input_fields_are_present(type, ast_node)
96
96
  # TODO - would be nice to use these to create an error message so the caller knows
97
97
  # that required fields are missing
98
- required_field_names = @warden.arguments(type)
99
- .select { |f| f.type.kind.non_null? }
98
+ required_field_names = type.arguments.each_value
99
+ .select { |argument| argument.type.kind.non_null? && @warden.get_argument(type, argument.name) }
100
100
  .map(&:name)
101
+
101
102
  present_field_names = ast_node.arguments.map(&:name)
102
103
  missing_required_field_names = required_field_names - present_field_names
103
104
  if @context.schema.error_bubbling
104
105
  missing_required_field_names.empty? ? @valid_response : @invalid_response
105
106
  else
106
107
  results = missing_required_field_names.map do |name|
107
- arg_type = @warden.arguments(type).find { |f| f.name == name }.type
108
+ arg_type = @warden.get_argument(type, name).type
108
109
  recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
109
110
  end
110
111
  merge_results(results)
@@ -112,13 +113,12 @@ module GraphQL
112
113
  end
113
114
 
114
115
  def present_input_field_values_are_valid(type, ast_node)
115
- field_map = @warden.arguments(type).reduce({}) { |m, f| m[f.name] = f; m}
116
116
  results = ast_node.arguments.map do |value|
117
- field = field_map[value.name]
117
+ field = @warden.get_argument(type, value.name)
118
118
  # we want to call validate on an argument even if it's an invalid one
119
119
  # so that our raise exception is on it instead of the entire InputObject
120
- type = field && field.type
121
- recursively_validate(value.value, type)
120
+ field_type = field && field.type
121
+ recursively_validate(value.value, field_type)
122
122
  end
123
123
  merge_results(results)
124
124
  end
@@ -5,7 +5,7 @@ module GraphQL
5
5
  def on_argument(node, parent)
6
6
  parent_defn = parent_definition(parent)
7
7
 
8
- if parent_defn && context.warden.arguments(parent_defn).any? { |arg| arg.name == node.name }
8
+ if parent_defn && context.warden.get_argument(parent_defn, node.name)
9
9
  super
10
10
  elsif parent_defn
11
11
  kind_of_node = node_type(parent)
@@ -17,8 +17,8 @@ module GraphQL
17
17
 
18
18
  def assert_required_args(ast_node, defn)
19
19
  present_argument_names = ast_node.arguments.map(&:name)
20
- required_argument_names = context.warden.arguments(defn)
21
- .select { |a| a.type.kind.non_null? && !a.default_value? }
20
+ required_argument_names = defn.arguments.each_value
21
+ .select { |a| a.type.kind.non_null? && !a.default_value? && context.warden.get_argument(defn, a.name) }
22
22
  .map(&:name)
23
23
 
24
24
  missing_names = required_argument_names - present_argument_names
@@ -26,8 +26,7 @@ module GraphQL
26
26
  context.field_definition
27
27
  end
28
28
 
29
- parent_type = context.warden.arguments(defn)
30
- .find{|f| f.name == parent_name(parent, defn) }
29
+ parent_type = context.warden.get_argument(defn, parent_name(parent, defn))
31
30
  parent_type ? parent_type.type.unwrap : nil
32
31
  end
33
32
 
@@ -126,8 +126,9 @@ module GraphQL
126
126
  node_variables
127
127
  .select { |name, usage| usage.declared? && !usage.used? }
128
128
  .each { |var_name, usage|
129
+ declared_by_error_name = usage.declared_by.name || "anonymous #{usage.declared_by.operation_type}"
129
130
  add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
130
- "Variable $#{var_name} is declared by #{usage.declared_by.name} but not used",
131
+ "Variable $#{var_name} is declared by #{declared_by_error_name} but not used",
131
132
  nodes: usage.declared_by,
132
133
  path: usage.path,
133
134
  name: var_name,
@@ -139,8 +140,9 @@ module GraphQL
139
140
  node_variables
140
141
  .select { |name, usage| usage.used? && !usage.declared? }
141
142
  .each { |var_name, usage|
143
+ used_by_error_name = usage.used_by.name || "anonymous #{usage.used_by.operation_type}"
142
144
  add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
143
- "Variable $#{var_name} is used by #{usage.used_by.name} but not declared",
145
+ "Variable $#{var_name} is used by #{used_by_error_name} but not declared",
144
146
  nodes: usage.ast_node,
145
147
  path: usage.path,
146
148
  name: var_name,
@@ -165,24 +165,35 @@ module GraphQL
165
165
  # Return the query from "storage" (in memory)
166
166
  def read_subscription(subscription_id)
167
167
  query = @subscriptions[subscription_id]
168
- {
169
- query_string: query.query_string,
170
- variables: query.provided_variables,
171
- context: query.context.to_h,
172
- operation_name: query.operation_name,
173
- }
168
+ if query.nil?
169
+ # This can happen when a subscription is triggered from an unsubscribed channel,
170
+ # see https://github.com/rmosolgo/graphql-ruby/issues/2478.
171
+ # (This `nil` is handled by `#execute_update`)
172
+ nil
173
+ else
174
+ {
175
+ query_string: query.query_string,
176
+ variables: query.provided_variables,
177
+ context: query.context.to_h,
178
+ operation_name: query.operation_name,
179
+ }
180
+ end
174
181
  end
175
182
 
176
183
  # The channel was closed, forget about it.
177
184
  def delete_subscription(subscription_id)
178
185
  query = @subscriptions.delete(subscription_id)
179
- events = query.context.namespace(:subscriptions)[:events]
180
- events.each do |event|
181
- ev_by_fingerprint = @events[event.topic]
182
- ev_for_fingerprint = ev_by_fingerprint[event.fingerprint]
183
- ev_for_fingerprint.delete(event)
184
- if ev_for_fingerprint.empty?
185
- ev_by_fingerprint.delete(event.fingerprint)
186
+ # This can be `nil` when `.trigger` happens inside an unsubscribed ActionCable channel,
187
+ # see https://github.com/rmosolgo/graphql-ruby/issues/2478
188
+ if query
189
+ events = query.context.namespace(:subscriptions)[:events]
190
+ events.each do |event|
191
+ ev_by_fingerprint = @events[event.topic]
192
+ ev_for_fingerprint = ev_by_fingerprint[event.fingerprint]
193
+ ev_for_fingerprint.delete(event)
194
+ if ev_for_fingerprint.empty?
195
+ ev_by_fingerprint.delete(event.fingerprint)
196
+ end
186
197
  end
187
198
  end
188
199
  end
@@ -9,6 +9,9 @@ module GraphQL
9
9
  GLOBALID_KEY = "__gid__"
10
10
  SYMBOL_KEY = "__sym__"
11
11
  SYMBOL_KEYS_KEY = "__sym_keys__"
12
+ TIMESTAMP_KEY = "__timestamp__"
13
+ TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%Z" # eg '2020-01-01 23:59:59.123456789+05:00'
14
+ OPEN_STRUCT_KEY = "__ostruct__"
12
15
 
13
16
  module_function
14
17
 
@@ -55,10 +58,20 @@ module GraphQL
55
58
  if value.is_a?(Array)
56
59
  value.map{|item| load_value(item)}
57
60
  elsif value.is_a?(Hash)
58
- if value.size == 1 && value.key?(GLOBALID_KEY)
59
- GlobalID::Locator.locate(value[GLOBALID_KEY])
60
- elsif value.size == 1 && value.key?(SYMBOL_KEY)
61
- value[SYMBOL_KEY].to_sym
61
+ if value.size == 1
62
+ case value.keys.first # there's only 1 key
63
+ when GLOBALID_KEY
64
+ GlobalID::Locator.locate(value[GLOBALID_KEY])
65
+ when SYMBOL_KEY
66
+ value[SYMBOL_KEY].to_sym
67
+ when TIMESTAMP_KEY
68
+ timestamp_class_name, timestamp_s = value[TIMESTAMP_KEY]
69
+ timestamp_class = Object.const_get(timestamp_class_name)
70
+ timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT)
71
+ when OPEN_STRUCT_KEY
72
+ ostruct_values = load_value(value[OPEN_STRUCT_KEY])
73
+ OpenStruct.new(ostruct_values)
74
+ end
62
75
  else
63
76
  loaded_h = {}
64
77
  sym_keys = value.fetch(SYMBOL_KEYS_KEY, [])
@@ -101,6 +114,11 @@ module GraphQL
101
114
  { SYMBOL_KEY => obj.to_s }
102
115
  elsif obj.respond_to?(:to_gid_param)
103
116
  {GLOBALID_KEY => obj.to_gid_param}
117
+ elsif obj.is_a?(Date) || obj.is_a?(Time)
118
+ # DateTime extends Date; for TimeWithZone, call `.utc` first.
119
+ { TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
120
+ elsif obj.is_a?(OpenStruct)
121
+ { OPEN_STRUCT_KEY => dump_value(obj.to_h) }
104
122
  else
105
123
  obj
106
124
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.11.1"
3
+ VERSION = "1.11.2"
4
4
  end
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.11.1
4
+ version: 1.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-17 00:00:00.000000000 Z
11
+ date: 2020-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips