graphql 1.11.1 → 1.11.2

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: 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