graphql 1.11.1 → 1.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +8 -0
  3. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  4. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  5. data/lib/generators/graphql/templates/base_field.erb +2 -0
  6. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  7. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  8. data/lib/generators/graphql/templates/base_mutation.erb +2 -0
  9. data/lib/generators/graphql/templates/base_object.erb +2 -0
  10. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  11. data/lib/generators/graphql/templates/base_union.erb +2 -0
  12. data/lib/generators/graphql/templates/enum.erb +2 -0
  13. data/lib/generators/graphql/templates/graphql_controller.erb +13 -9
  14. data/lib/generators/graphql/templates/interface.erb +2 -0
  15. data/lib/generators/graphql/templates/loader.erb +2 -0
  16. data/lib/generators/graphql/templates/mutation.erb +2 -0
  17. data/lib/generators/graphql/templates/mutation_type.erb +2 -0
  18. data/lib/generators/graphql/templates/object.erb +2 -0
  19. data/lib/generators/graphql/templates/query_type.erb +2 -0
  20. data/lib/generators/graphql/templates/scalar.erb +2 -0
  21. data/lib/generators/graphql/templates/schema.erb +2 -0
  22. data/lib/generators/graphql/templates/union.erb +3 -1
  23. data/lib/graphql.rb +16 -0
  24. data/lib/graphql/argument.rb +3 -3
  25. data/lib/graphql/backtrace/tracer.rb +2 -1
  26. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  27. data/lib/graphql/directive.rb +4 -0
  28. data/lib/graphql/execution/interpreter.rb +10 -0
  29. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  30. data/lib/graphql/execution/interpreter/runtime.rb +59 -45
  31. data/lib/graphql/field.rb +4 -0
  32. data/lib/graphql/input_object_type.rb +4 -0
  33. data/lib/graphql/introspection.rb +96 -0
  34. data/lib/graphql/introspection/field_type.rb +7 -3
  35. data/lib/graphql/introspection/input_value_type.rb +6 -0
  36. data/lib/graphql/introspection/introspection_query.rb +6 -92
  37. data/lib/graphql/introspection/type_type.rb +7 -3
  38. data/lib/graphql/language/block_string.rb +24 -5
  39. data/lib/graphql/language/lexer.rb +7 -3
  40. data/lib/graphql/language/lexer.rl +7 -3
  41. data/lib/graphql/language/nodes.rb +2 -1
  42. data/lib/graphql/language/parser.rb +107 -103
  43. data/lib/graphql/language/parser.y +4 -0
  44. data/lib/graphql/language/sanitized_printer.rb +59 -26
  45. data/lib/graphql/language/visitor.rb +2 -2
  46. data/lib/graphql/name_validator.rb +6 -7
  47. data/lib/graphql/pagination/connection.rb +6 -8
  48. data/lib/graphql/pagination/connections.rb +23 -3
  49. data/lib/graphql/query.rb +2 -2
  50. data/lib/graphql/query/context.rb +30 -3
  51. data/lib/graphql/query/fingerprint.rb +2 -0
  52. data/lib/graphql/query/validation_pipeline.rb +3 -0
  53. data/lib/graphql/relay/range_add.rb +14 -5
  54. data/lib/graphql/schema.rb +40 -31
  55. data/lib/graphql/schema/argument.rb +56 -5
  56. data/lib/graphql/schema/build_from_definition.rb +67 -38
  57. data/lib/graphql/schema/build_from_definition/resolve_map.rb +3 -1
  58. data/lib/graphql/schema/directive/deprecated.rb +1 -1
  59. data/lib/graphql/schema/enum_value.rb +1 -0
  60. data/lib/graphql/schema/field.rb +17 -10
  61. data/lib/graphql/schema/field/connection_extension.rb +44 -34
  62. data/lib/graphql/schema/input_object.rb +21 -18
  63. data/lib/graphql/schema/interface.rb +1 -1
  64. data/lib/graphql/schema/late_bound_type.rb +2 -2
  65. data/lib/graphql/schema/loader.rb +20 -1
  66. data/lib/graphql/schema/member/build_type.rb +14 -4
  67. data/lib/graphql/schema/member/has_arguments.rb +19 -1
  68. data/lib/graphql/schema/member/has_fields.rb +17 -7
  69. data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
  70. data/lib/graphql/schema/mutation.rb +4 -0
  71. data/lib/graphql/schema/relay_classic_mutation.rb +3 -1
  72. data/lib/graphql/schema/resolver.rb +6 -0
  73. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -1
  74. data/lib/graphql/schema/subscription.rb +2 -12
  75. data/lib/graphql/schema/timeout.rb +29 -15
  76. data/lib/graphql/schema/union.rb +29 -0
  77. data/lib/graphql/schema/unique_within_type.rb +1 -2
  78. data/lib/graphql/schema/validation.rb +8 -0
  79. data/lib/graphql/schema/warden.rb +8 -3
  80. data/lib/graphql/static_validation/literal_validator.rb +7 -7
  81. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
  82. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
  83. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -2
  84. data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +4 -2
  85. data/lib/graphql/static_validation/validator.rb +7 -4
  86. data/lib/graphql/subscriptions.rb +32 -22
  87. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +45 -20
  88. data/lib/graphql/subscriptions/serialize.rb +22 -4
  89. data/lib/graphql/tracing/appoptics_tracing.rb +10 -2
  90. data/lib/graphql/types/iso_8601_date_time.rb +2 -1
  91. data/lib/graphql/types/relay/base_connection.rb +6 -5
  92. data/lib/graphql/unauthorized_error.rb +1 -1
  93. data/lib/graphql/version.rb +1 -1
  94. metadata +3 -3
@@ -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
@@ -27,8 +27,7 @@ module GraphQL
27
27
  # @param node_id [String] A unique ID generated by {.encode}
28
28
  # @return [Array<(String, String)>] The type name & value passed to {.encode}
29
29
  def decode(node_id, separator: self.default_id_separator)
30
- # urlsafe_decode64 is for forward compatibility
31
- Base64Bp.urlsafe_decode64(node_id).split(separator, 2)
30
+ GraphQL::Schema::Base64Encoder.decode(node_id).split(separator, 2)
32
31
  end
33
32
  end
34
33
  end
@@ -133,6 +133,12 @@ module GraphQL
133
133
  end
134
134
  }
135
135
 
136
+ DEPRECATED_ARGUMENTS_ARE_OPTIONAL = ->(argument) {
137
+ if argument.deprecation_reason && argument.type.non_null?
138
+ "must be optional because it's deprecated"
139
+ end
140
+ }
141
+
136
142
  TYPE_IS_VALID_INPUT_TYPE = ->(type) {
137
143
  outer_type = type.type
138
144
  inner_type = outer_type.respond_to?(:unwrap) ? outer_type.unwrap : nil
@@ -265,8 +271,10 @@ module GraphQL
265
271
  Rules::NAME_IS_STRING,
266
272
  Rules::RESERVED_NAME,
267
273
  Rules::DESCRIPTION_IS_STRING_OR_NIL,
274
+ Rules.assert_property(:deprecation_reason, String, NilClass),
268
275
  Rules::TYPE_IS_VALID_INPUT_TYPE,
269
276
  Rules::DEFAULT_VALUE_IS_VALID_FOR_TYPE,
277
+ Rules::DEPRECATED_ARGUMENTS_ARE_OPTIONAL,
270
278
  ],
271
279
  GraphQL::BaseType => [
272
280
  Rules::NAME_IS_STRING,
@@ -40,7 +40,6 @@ module GraphQL
40
40
  # @param filter [<#call(member)>] Objects are hidden when `.call(member, ctx)` returns true
41
41
  # @param context [GraphQL::Query::Context]
42
42
  # @param schema [GraphQL::Schema]
43
- # @param deep_check [Boolean]
44
43
  def initialize(filter, context:, schema:)
45
44
  @schema = schema.interpreter? ? schema : schema.graphql_definition
46
45
  # Cache these to avoid repeated hits to the inheritance chain when one isn't present
@@ -51,7 +50,7 @@ module GraphQL
51
50
  @visibility_cache = read_through { |m| filter.call(m, context) }
52
51
  end
53
52
 
54
- # @return [Array<GraphQL::BaseType>] Visible types in the schema
53
+ # @return [Hash<String, GraphQL::BaseType>] Visible types in the schema
55
54
  def types
56
55
  @types ||= begin
57
56
  vis_types = {}
@@ -105,6 +104,12 @@ module GraphQL
105
104
  @visible_parent_fields[parent_type][field_name]
106
105
  end
107
106
 
107
+ # @return [GraphQL::Argument, nil] The argument named `argument_name` on `parent_type`, if it exists and is visible
108
+ def get_argument(parent_type, argument_name)
109
+ argument = parent_type.get_argument(argument_name)
110
+ return argument if argument && visible_argument?(argument)
111
+ end
112
+
108
113
  # @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
109
114
  def possible_types(type_defn)
110
115
  @visible_possible_types ||= read_through { |type_defn|
@@ -193,7 +198,7 @@ module GraphQL
193
198
  if (iface_field_defn = interface_type.get_field(field_defn.graphql_name))
194
199
  any_interface_has_field = true
195
200
 
196
- if visible?(interface_type) && visible_field?(interface_type, iface_field_defn)
201
+ if interfaces(type_defn).include?(interface_type) && visible_field?(interface_type, iface_field_defn)
197
202
  any_interface_has_visible_field = true
198
203
  end
199
204
  end
@@ -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,
@@ -32,10 +32,13 @@ module GraphQL
32
32
 
33
33
  context = GraphQL::StaticValidation::ValidationContext.new(query, visitor_class)
34
34
 
35
- # Attach legacy-style rules
36
- rules_to_use.each do |rule_class_or_module|
37
- if rule_class_or_module.method_defined?(:validate)
38
- rule_class_or_module.new.validate(context)
35
+ # Attach legacy-style rules.
36
+ # Only loop through rules if it has legacy-style rules
37
+ unless (legacy_rules = rules_to_use - GraphQL::StaticValidation::ALL_RULES).empty?
38
+ legacy_rules.each do |rule_class_or_module|
39
+ if rule_class_or_module.method_defined?(:validate)
40
+ rule_class_or_module.new.validate(context)
41
+ end
39
42
  end
40
43
  end
41
44
 
@@ -4,9 +4,7 @@ require "graphql/subscriptions/broadcast_analyzer"
4
4
  require "graphql/subscriptions/event"
5
5
  require "graphql/subscriptions/instrumentation"
6
6
  require "graphql/subscriptions/serialize"
7
- if defined?(ActionCable)
8
- require "graphql/subscriptions/action_cable_subscriptions"
9
- end
7
+ require "graphql/subscriptions/action_cable_subscriptions"
10
8
  require "graphql/subscriptions/subscription_root"
11
9
  require "graphql/subscriptions/default_subscription_resolve_extension"
12
10
 
@@ -100,31 +98,43 @@ module GraphQL
100
98
  # Lookup the saved data for this subscription
101
99
  query_data = read_subscription(subscription_id)
102
100
  if query_data.nil?
103
- # Jump down to the `delete_subscription` call
104
- raise GraphQL::Schema::Subscription::UnsubscribedError
101
+ delete_subscription(subscription_id)
102
+ return nil
105
103
  end
104
+
106
105
  # Fetch the required keys from the saved data
107
106
  query_string = query_data.fetch(:query_string)
108
107
  variables = query_data.fetch(:variables)
109
108
  context = query_data.fetch(:context)
110
109
  operation_name = query_data.fetch(:operation_name)
111
- # Re-evaluate the saved query
112
- @schema.execute(
113
- query: query_string,
114
- context: context,
115
- subscription_topic: event.topic,
116
- operation_name: operation_name,
117
- variables: variables,
118
- root_value: object,
119
- )
120
- rescue GraphQL::Schema::Subscription::NoUpdateError
121
- # This update was skipped in user code; do nothing.
122
- nil
123
- rescue GraphQL::Schema::Subscription::UnsubscribedError
124
- # `unsubscribe` was called, clean up on our side
125
- # TODO also send `{more: false}` to client?
126
- delete_subscription(subscription_id)
127
- nil
110
+ result = nil
111
+ # this will be set to `false` unless `.execute` is terminated
112
+ # with a `throw :graphql_subscription_unsubscribed`
113
+ unsubscribed = true
114
+ catch(:graphql_subscription_unsubscribed) do
115
+ catch(:graphql_no_subscription_update) do
116
+ # Re-evaluate the saved query,
117
+ # but if it terminates early with a `throw`,
118
+ # it will stay `nil`
119
+ result = @schema.execute(
120
+ query: query_string,
121
+ context: context,
122
+ subscription_topic: event.topic,
123
+ operation_name: operation_name,
124
+ variables: variables,
125
+ root_value: object,
126
+ )
127
+ end
128
+ unsubscribed = false
129
+ end
130
+
131
+ if unsubscribed
132
+ # `unsubscribe` was called, clean up on our side
133
+ # TODO also send `{more: false}` to client?
134
+ delete_subscription(subscription_id)
135
+ end
136
+
137
+ result
128
138
  end
129
139
 
130
140
  # Run the update query for this subscription and deliver it
@@ -4,7 +4,7 @@ module GraphQL
4
4
  # A subscriptions implementation that sends data
5
5
  # as ActionCable broadcastings.
6
6
  #
7
- # Experimental, some things to keep in mind:
7
+ # Some things to keep in mind:
8
8
  #
9
9
  # - No queueing system; ActiveJob should be added
10
10
  # - Take care to reload context when re-delivering the subscription. (see {Query#subscription_update?})
@@ -86,28 +86,32 @@ module GraphQL
86
86
  EVENT_PREFIX = "graphql-event:"
87
87
 
88
88
  # @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
89
- def initialize(serializer: Serialize, **rest)
89
+ # @param namespace [string] Used to namespace events and subscriptions (default: '')
90
+ def initialize(serializer: Serialize, namespace: '', action_cable: ActionCable, action_cable_coder: ActiveSupport::JSON, **rest)
90
91
  # A per-process map of subscriptions to deliver.
91
92
  # This is provided by Rails, so let's use it
92
93
  @subscriptions = Concurrent::Map.new
93
94
  @events = Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new { |h2, k2| h2[k2] = Concurrent::Array.new } }
95
+ @action_cable = action_cable
96
+ @action_cable_coder = action_cable_coder
94
97
  @serializer = serializer
98
+ @transmit_ns = namespace
95
99
  super
96
100
  end
97
101
 
98
102
  # An event was triggered; Push the data over ActionCable.
99
103
  # Subscribers will re-evaluate locally.
100
104
  def execute_all(event, object)
101
- stream = EVENT_PREFIX + event.topic
105
+ stream = stream_event_name(event)
102
106
  message = @serializer.dump(object)
103
- ActionCable.server.broadcast(stream, message)
107
+ @action_cable.server.broadcast(stream, message)
104
108
  end
105
109
 
106
110
  # This subscription was re-evaluated.
107
111
  # Send it to the specific stream where this client was waiting.
108
112
  def deliver(subscription_id, result)
109
113
  payload = { result: result.to_h, more: true }
110
- ActionCable.server.broadcast(SUBSCRIPTION_PREFIX + subscription_id, payload)
114
+ @action_cable.server.broadcast(stream_subscription_name(subscription_id), payload)
111
115
  end
112
116
 
113
117
  # A query was run where these events were subscribed to.
@@ -117,7 +121,7 @@ module GraphQL
117
121
  def write_subscription(query, events)
118
122
  channel = query.context.fetch(:channel)
119
123
  subscription_id = query.context[:subscription_id] ||= build_id
120
- stream = query.context[:action_cable_stream] ||= SUBSCRIPTION_PREFIX + subscription_id
124
+ stream = stream_subscription_name(subscription_id)
121
125
  channel.stream_from(stream)
122
126
  @subscriptions[subscription_id] = query
123
127
  events.each do |event|
@@ -141,7 +145,7 @@ module GraphQL
141
145
  #
142
146
  def setup_stream(channel, initial_event)
143
147
  topic = initial_event.topic
144
- channel.stream_from(EVENT_PREFIX + topic, coder: ActiveSupport::JSON) do |message|
148
+ channel.stream_from(stream_event_name(initial_event), coder: @action_cable_coder) do |message|
145
149
  object = @serializer.load(message)
146
150
  events_by_fingerprint = @events[topic]
147
151
  events_by_fingerprint.each do |_fingerprint, events|
@@ -165,27 +169,48 @@ module GraphQL
165
169
  # Return the query from "storage" (in memory)
166
170
  def read_subscription(subscription_id)
167
171
  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
- }
172
+ if query.nil?
173
+ # This can happen when a subscription is triggered from an unsubscribed channel,
174
+ # see https://github.com/rmosolgo/graphql-ruby/issues/2478.
175
+ # (This `nil` is handled by `#execute_update`)
176
+ nil
177
+ else
178
+ {
179
+ query_string: query.query_string,
180
+ variables: query.provided_variables,
181
+ context: query.context.to_h,
182
+ operation_name: query.operation_name,
183
+ }
184
+ end
174
185
  end
175
186
 
176
187
  # The channel was closed, forget about it.
177
188
  def delete_subscription(subscription_id)
178
189
  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)
190
+ # This can be `nil` when `.trigger` happens inside an unsubscribed ActionCable channel,
191
+ # see https://github.com/rmosolgo/graphql-ruby/issues/2478
192
+ if query
193
+ events = query.context.namespace(:subscriptions)[:events]
194
+ events.each do |event|
195
+ ev_by_fingerprint = @events[event.topic]
196
+ ev_for_fingerprint = ev_by_fingerprint[event.fingerprint]
197
+ ev_for_fingerprint.delete(event)
198
+ if ev_for_fingerprint.empty?
199
+ ev_by_fingerprint.delete(event.fingerprint)
200
+ end
186
201
  end
187
202
  end
188
203
  end
204
+
205
+ private
206
+
207
+ def stream_subscription_name(subscription_id)
208
+ [SUBSCRIPTION_PREFIX, @transmit_ns, subscription_id].join
209
+ end
210
+
211
+ def stream_event_name(event)
212
+ [EVENT_PREFIX, @transmit_ns, event.topic].join
213
+ end
189
214
  end
190
215
  end
191
216
  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