graphql 1.11.1 → 1.11.6

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