graphql 1.11.1 → 1.11.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphql/core.rb +8 -0
- data/lib/generators/graphql/templates/base_argument.erb +2 -0
- data/lib/generators/graphql/templates/base_enum.erb +2 -0
- data/lib/generators/graphql/templates/base_field.erb +2 -0
- data/lib/generators/graphql/templates/base_input_object.erb +2 -0
- data/lib/generators/graphql/templates/base_interface.erb +2 -0
- data/lib/generators/graphql/templates/base_mutation.erb +2 -0
- data/lib/generators/graphql/templates/base_object.erb +2 -0
- data/lib/generators/graphql/templates/base_scalar.erb +2 -0
- data/lib/generators/graphql/templates/base_union.erb +2 -0
- data/lib/generators/graphql/templates/enum.erb +2 -0
- data/lib/generators/graphql/templates/graphql_controller.erb +13 -9
- data/lib/generators/graphql/templates/interface.erb +2 -0
- data/lib/generators/graphql/templates/loader.erb +2 -0
- data/lib/generators/graphql/templates/mutation.erb +2 -0
- data/lib/generators/graphql/templates/mutation_type.erb +2 -0
- data/lib/generators/graphql/templates/object.erb +2 -0
- data/lib/generators/graphql/templates/query_type.erb +2 -0
- data/lib/generators/graphql/templates/scalar.erb +2 -0
- data/lib/generators/graphql/templates/schema.erb +2 -0
- data/lib/generators/graphql/templates/union.erb +3 -1
- data/lib/graphql.rb +16 -0
- data/lib/graphql/argument.rb +3 -3
- data/lib/graphql/backtrace/tracer.rb +2 -1
- data/lib/graphql/define/assign_global_id_field.rb +2 -2
- data/lib/graphql/directive.rb +4 -0
- data/lib/graphql/execution/interpreter.rb +10 -0
- data/lib/graphql/execution/interpreter/arguments.rb +1 -1
- data/lib/graphql/execution/interpreter/runtime.rb +59 -45
- data/lib/graphql/field.rb +4 -0
- data/lib/graphql/input_object_type.rb +4 -0
- data/lib/graphql/introspection.rb +96 -0
- data/lib/graphql/introspection/field_type.rb +7 -3
- data/lib/graphql/introspection/input_value_type.rb +6 -0
- data/lib/graphql/introspection/introspection_query.rb +6 -92
- data/lib/graphql/introspection/type_type.rb +7 -3
- data/lib/graphql/language/block_string.rb +24 -5
- data/lib/graphql/language/lexer.rb +7 -3
- data/lib/graphql/language/lexer.rl +7 -3
- data/lib/graphql/language/nodes.rb +2 -1
- data/lib/graphql/language/parser.rb +107 -103
- data/lib/graphql/language/parser.y +4 -0
- data/lib/graphql/language/sanitized_printer.rb +59 -26
- data/lib/graphql/language/visitor.rb +2 -2
- data/lib/graphql/name_validator.rb +6 -7
- data/lib/graphql/pagination/connection.rb +6 -8
- data/lib/graphql/pagination/connections.rb +23 -3
- data/lib/graphql/query.rb +2 -2
- data/lib/graphql/query/context.rb +30 -3
- data/lib/graphql/query/fingerprint.rb +2 -0
- data/lib/graphql/query/validation_pipeline.rb +3 -0
- data/lib/graphql/relay/range_add.rb +14 -5
- data/lib/graphql/schema.rb +40 -31
- data/lib/graphql/schema/argument.rb +56 -5
- data/lib/graphql/schema/build_from_definition.rb +67 -38
- data/lib/graphql/schema/build_from_definition/resolve_map.rb +3 -1
- data/lib/graphql/schema/directive/deprecated.rb +1 -1
- data/lib/graphql/schema/enum_value.rb +1 -0
- data/lib/graphql/schema/field.rb +17 -10
- data/lib/graphql/schema/field/connection_extension.rb +44 -34
- data/lib/graphql/schema/input_object.rb +21 -18
- data/lib/graphql/schema/interface.rb +1 -1
- data/lib/graphql/schema/late_bound_type.rb +2 -2
- data/lib/graphql/schema/loader.rb +20 -1
- data/lib/graphql/schema/member/build_type.rb +14 -4
- data/lib/graphql/schema/member/has_arguments.rb +19 -1
- data/lib/graphql/schema/member/has_fields.rb +17 -7
- data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
- data/lib/graphql/schema/mutation.rb +4 -0
- data/lib/graphql/schema/relay_classic_mutation.rb +3 -1
- data/lib/graphql/schema/resolver.rb +6 -0
- data/lib/graphql/schema/resolver/has_payload_type.rb +2 -1
- data/lib/graphql/schema/subscription.rb +2 -12
- data/lib/graphql/schema/timeout.rb +29 -15
- data/lib/graphql/schema/union.rb +29 -0
- data/lib/graphql/schema/unique_within_type.rb +1 -2
- data/lib/graphql/schema/validation.rb +8 -0
- data/lib/graphql/schema/warden.rb +8 -3
- data/lib/graphql/static_validation/literal_validator.rb +7 -7
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -2
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +4 -2
- data/lib/graphql/static_validation/validator.rb +7 -4
- data/lib/graphql/subscriptions.rb +32 -22
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +45 -20
- data/lib/graphql/subscriptions/serialize.rb +22 -4
- data/lib/graphql/tracing/appoptics_tracing.rb +10 -2
- data/lib/graphql/types/iso_8601_date_time.rb +2 -1
- data/lib/graphql/types/relay/base_connection.rb +6 -5
- data/lib/graphql/unauthorized_error.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- metadata +3 -3
data/lib/graphql/schema/union.rb
CHANGED
@@ -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
|
-
|
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 [
|
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
|
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 =
|
99
|
-
.select { |
|
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.
|
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 =
|
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
|
-
|
121
|
-
recursively_validate(value.value,
|
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.
|
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 =
|
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.
|
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 #{
|
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 #{
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
104
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
#
|
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
|
-
|
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 =
|
105
|
+
stream = stream_event_name(event)
|
102
106
|
message = @serializer.dump(object)
|
103
|
-
|
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
|
-
|
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 =
|
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(
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
ev_by_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
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|