graphql 1.10.14 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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 +2 -0
- data/lib/graphql.rb +3 -3
- data/lib/graphql/execution/interpreter.rb +1 -1
- data/lib/graphql/execution/interpreter/runtime.rb +26 -26
- data/lib/graphql/execution/multiplex.rb +1 -2
- data/lib/graphql/introspection/schema_type.rb +3 -3
- data/lib/graphql/invalid_null_error.rb +18 -0
- data/lib/graphql/language/nodes.rb +1 -0
- data/lib/graphql/language/visitor.rb +2 -2
- data/lib/graphql/pagination/connection.rb +18 -13
- data/lib/graphql/pagination/connections.rb +17 -4
- data/lib/graphql/query.rb +1 -2
- data/lib/graphql/query/context.rb +20 -1
- data/lib/graphql/query/fingerprint.rb +2 -0
- data/lib/graphql/query/validation_pipeline.rb +3 -0
- data/lib/graphql/schema.rb +26 -16
- data/lib/graphql/schema/build_from_definition.rb +7 -12
- data/lib/graphql/schema/build_from_definition/resolve_map.rb +3 -1
- data/lib/graphql/schema/enum_value.rb +1 -0
- data/lib/graphql/schema/field.rb +63 -77
- data/lib/graphql/schema/field/connection_extension.rb +42 -32
- data/lib/graphql/schema/loader.rb +19 -1
- data/lib/graphql/schema/member/has_fields.rb +15 -5
- data/lib/graphql/schema/mutation.rb +4 -0
- data/lib/graphql/schema/object.rb +1 -1
- data/lib/graphql/schema/resolver.rb +20 -0
- data/lib/graphql/schema/resolver/has_payload_type.rb +2 -1
- data/lib/graphql/schema/subscription.rb +3 -13
- data/lib/graphql/schema/union.rb +29 -0
- data/lib/graphql/schema/warden.rb +2 -4
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +4 -2
- data/lib/graphql/subscriptions.rb +69 -24
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +66 -11
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
- data/lib/graphql/subscriptions/event.rb +16 -1
- data/lib/graphql/subscriptions/serialize.rb +22 -4
- data/lib/graphql/subscriptions/subscription_root.rb +3 -1
- data/lib/graphql/tracing.rb +1 -27
- data/lib/graphql/tracing/appoptics_tracing.rb +10 -2
- data/lib/graphql/tracing/platform_tracing.rb +25 -15
- data/lib/graphql/tracing/statsd_tracing.rb +42 -0
- data/lib/graphql/types/iso_8601_date_time.rb +2 -1
- data/lib/graphql/types/relay/base_connection.rb +6 -5
- data/lib/graphql/version.rb +1 -1
- metadata +5 -2
@@ -22,39 +22,49 @@ module GraphQL
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def after_resolve(value:, object:, arguments:, context:, memo:)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
value.
|
35
|
-
|
36
|
-
value.
|
37
|
-
|
38
|
-
|
39
|
-
value.
|
25
|
+
# rename some inputs to avoid conflicts inside the block
|
26
|
+
maybe_lazy = value
|
27
|
+
value = nil
|
28
|
+
context.schema.after_lazy(maybe_lazy) do |resolved_value|
|
29
|
+
value = resolved_value
|
30
|
+
if value.is_a? GraphQL::ExecutionError
|
31
|
+
# This isn't even going to work because context doesn't have ast_node anymore
|
32
|
+
context.add_error(value)
|
33
|
+
nil
|
34
|
+
elsif value.nil?
|
35
|
+
nil
|
36
|
+
elsif value.is_a?(GraphQL::Pagination::Connection)
|
37
|
+
# update the connection with some things that may not have been provided
|
38
|
+
value.context ||= context
|
39
|
+
value.parent ||= object.object
|
40
|
+
value.first_value ||= arguments[:first]
|
41
|
+
value.after_value ||= arguments[:after]
|
42
|
+
value.last_value ||= arguments[:last]
|
43
|
+
value.before_value ||= arguments[:before]
|
44
|
+
if field.has_max_page_size? && !value.has_max_page_size_override?
|
45
|
+
value.max_page_size = field.max_page_size
|
46
|
+
end
|
47
|
+
if context.schema.new_connections? && (custom_t = context.schema.connections.edge_class_for_field(@field))
|
48
|
+
value.edge_class = custom_t
|
49
|
+
end
|
50
|
+
value
|
51
|
+
elsif context.schema.new_connections?
|
52
|
+
wrappers = context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
|
53
|
+
context.schema.connections.wrap(field, object.object, value, arguments, context, wrappers: wrappers)
|
54
|
+
else
|
55
|
+
if object.is_a?(GraphQL::Schema::Object)
|
56
|
+
object = object.object
|
57
|
+
end
|
58
|
+
connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(value)
|
59
|
+
connection_class.new(
|
60
|
+
value,
|
61
|
+
arguments,
|
62
|
+
field: field,
|
63
|
+
max_page_size: field.max_page_size,
|
64
|
+
parent: object,
|
65
|
+
context: context,
|
66
|
+
)
|
40
67
|
end
|
41
|
-
value
|
42
|
-
elsif context.schema.new_connections?
|
43
|
-
wrappers = context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers
|
44
|
-
context.schema.connections.wrap(field, value, arguments, context, wrappers: wrappers)
|
45
|
-
else
|
46
|
-
if object.is_a?(GraphQL::Schema::Object)
|
47
|
-
object = object.object
|
48
|
-
end
|
49
|
-
connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(value)
|
50
|
-
connection_class.new(
|
51
|
-
value,
|
52
|
-
arguments,
|
53
|
-
field: field,
|
54
|
-
max_page_size: field.max_page_size,
|
55
|
-
parent: object,
|
56
|
-
context: context,
|
57
|
-
)
|
58
68
|
end
|
59
69
|
end
|
60
70
|
end
|
@@ -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["
|
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
|
@@ -47,20 +47,22 @@ module GraphQL
|
|
47
47
|
# A list of GraphQL-Ruby keywords.
|
48
48
|
#
|
49
49
|
# @api private
|
50
|
-
GRAPHQL_RUBY_KEYWORDS = [:context, :object, :
|
50
|
+
GRAPHQL_RUBY_KEYWORDS = [:context, :object, :raw_value]
|
51
51
|
|
52
52
|
# A list of field names that we should advise users to pick a different
|
53
53
|
# resolve method name.
|
54
54
|
#
|
55
55
|
# @api private
|
56
|
-
CONFLICT_FIELD_NAMES = Set.new(GRAPHQL_RUBY_KEYWORDS + RUBY_KEYWORDS)
|
56
|
+
CONFLICT_FIELD_NAMES = Set.new(GRAPHQL_RUBY_KEYWORDS + RUBY_KEYWORDS + Object.instance_methods)
|
57
57
|
|
58
58
|
# Register this field with the class, overriding a previous one if needed.
|
59
59
|
# @param field_defn [GraphQL::Schema::Field]
|
60
60
|
# @return [void]
|
61
|
-
def add_field(field_defn)
|
62
|
-
|
63
|
-
|
61
|
+
def add_field(field_defn, method_conflict_warning: field_defn.method_conflict_warning?)
|
62
|
+
# Check that `field_defn.original_name` equals `resolver_method` and `method_sym` --
|
63
|
+
# that shows that no override value was given manually.
|
64
|
+
if method_conflict_warning && CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) && field_defn.original_name == field_defn.resolver_method && field_defn.original_name == field_defn.method_sym
|
65
|
+
warn(conflict_field_name_warning(field_defn))
|
64
66
|
end
|
65
67
|
own_fields[field_defn.name] = field_defn
|
66
68
|
nil
|
@@ -92,6 +94,14 @@ module GraphQL
|
|
92
94
|
def own_fields
|
93
95
|
@own_fields ||= {}
|
94
96
|
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# @param [GraphQL::Schema::Field]
|
101
|
+
# @return [String] A warning to give when this field definition might conflict with a built-in method
|
102
|
+
def conflict_field_name_warning(field_defn)
|
103
|
+
"#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
|
104
|
+
end
|
95
105
|
end
|
96
106
|
end
|
97
107
|
end
|
@@ -78,6 +78,10 @@ module GraphQL
|
|
78
78
|
|
79
79
|
private
|
80
80
|
|
81
|
+
def conflict_field_name_warning(field_defn)
|
82
|
+
"#{self.graphql_name}'s `field :#{field_defn.name}` conflicts with a built-in method, use `hash_key:` or `method:` to pick a different resolve behavior for this field (for example, `hash_key: :#{field_defn.resolver_method}_value`, and modify the return hash). Or use `method_conflict_warning: false` to suppress this warning."
|
83
|
+
end
|
84
|
+
|
81
85
|
# Override this to attach self as `mutation`
|
82
86
|
def generate_payload_type
|
83
87
|
payload_class = super
|
@@ -74,7 +74,7 @@ module GraphQL
|
|
74
74
|
# Set up a type-specific invalid null error to use when this object's non-null fields wrongly return `nil`.
|
75
75
|
# It should help with debugging and bug tracker integrations.
|
76
76
|
def inherited(child_class)
|
77
|
-
child_class.const_set(:InvalidNullError,
|
77
|
+
child_class.const_set(:InvalidNullError, GraphQL::InvalidNullError.subclass_for(child_class))
|
78
78
|
super
|
79
79
|
end
|
80
80
|
|
@@ -40,6 +40,7 @@ module GraphQL
|
|
40
40
|
@arguments_by_keyword[arg.keyword] = arg
|
41
41
|
end
|
42
42
|
@arguments_loads_as_type = self.class.arguments_loads_as_type
|
43
|
+
@prepared_arguments = nil
|
43
44
|
end
|
44
45
|
|
45
46
|
# @return [Object] The application object this field is being resolved on
|
@@ -51,6 +52,10 @@ module GraphQL
|
|
51
52
|
# @return [GraphQL::Schema::Field]
|
52
53
|
attr_reader :field
|
53
54
|
|
55
|
+
def arguments
|
56
|
+
@prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
|
57
|
+
end
|
58
|
+
|
54
59
|
# This method is _actually_ called by the runtime,
|
55
60
|
# it does some preparation and then eventually calls
|
56
61
|
# the user-defined `#resolve` method.
|
@@ -74,6 +79,7 @@ module GraphQL
|
|
74
79
|
# for that argument, or may return a lazy object
|
75
80
|
load_arguments_val = load_arguments(args)
|
76
81
|
context.schema.after_lazy(load_arguments_val) do |loaded_args|
|
82
|
+
@prepared_arguments = loaded_args
|
77
83
|
# Then call `authorized?`, which may raise or may return a lazy object
|
78
84
|
authorized_val = if loaded_args.any?
|
79
85
|
authorized?(**loaded_args)
|
@@ -250,6 +256,19 @@ module GraphQL
|
|
250
256
|
@complexity || (superclass.respond_to?(:complexity) ? superclass.complexity : 1)
|
251
257
|
end
|
252
258
|
|
259
|
+
def broadcastable(new_broadcastable)
|
260
|
+
@broadcastable = new_broadcastable
|
261
|
+
end
|
262
|
+
|
263
|
+
# @return [Boolean, nil]
|
264
|
+
def broadcastable?
|
265
|
+
if defined?(@broadcastable)
|
266
|
+
@broadcastable
|
267
|
+
else
|
268
|
+
(superclass.respond_to?(:broadcastable?) ? superclass.broadcastable? : nil)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
253
272
|
def field_options
|
254
273
|
{
|
255
274
|
type: type_expr,
|
@@ -261,6 +280,7 @@ module GraphQL
|
|
261
280
|
null: null,
|
262
281
|
complexity: complexity,
|
263
282
|
extensions: extensions,
|
283
|
+
broadcastable: broadcastable?,
|
264
284
|
}
|
265
285
|
end
|
266
286
|
|
@@ -58,7 +58,8 @@ module GraphQL
|
|
58
58
|
resolver_fields.each do |name, f|
|
59
59
|
# Reattach the already-defined field here
|
60
60
|
# (The field's `.owner` will still point to the mutation, not the object type, I think)
|
61
|
-
|
61
|
+
# Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead.
|
62
|
+
add_field(f, method_conflict_warning: false)
|
62
63
|
end
|
63
64
|
end
|
64
65
|
end
|
@@ -12,16 +12,6 @@ module GraphQL
|
|
12
12
|
#
|
13
13
|
# Also, `#unsubscribe` terminates the subscription.
|
14
14
|
class Subscription < GraphQL::Schema::Resolver
|
15
|
-
class EarlyTerminationError < StandardError
|
16
|
-
end
|
17
|
-
|
18
|
-
# Raised when `unsubscribe` is called; caught by `subscriptions.rb`
|
19
|
-
class UnsubscribedError < EarlyTerminationError
|
20
|
-
end
|
21
|
-
|
22
|
-
# Raised when `no_update` is returned; caught by `subscriptions.rb`
|
23
|
-
class NoUpdateError < EarlyTerminationError
|
24
|
-
end
|
25
15
|
extend GraphQL::Schema::Resolver::HasPayloadType
|
26
16
|
extend GraphQL::Schema::Member::HasFields
|
27
17
|
|
@@ -65,7 +55,7 @@ module GraphQL
|
|
65
55
|
def resolve_update(**args)
|
66
56
|
ret_val = args.any? ? update(**args) : update
|
67
57
|
if ret_val == :no_update
|
68
|
-
|
58
|
+
throw :graphql_no_subscription_update
|
69
59
|
else
|
70
60
|
ret_val
|
71
61
|
end
|
@@ -90,14 +80,14 @@ module GraphQL
|
|
90
80
|
|
91
81
|
# Call this to halt execution and remove this subscription from the system
|
92
82
|
def unsubscribe
|
93
|
-
|
83
|
+
throw :graphql_subscription_unsubscribed
|
94
84
|
end
|
95
85
|
|
86
|
+
READING_SCOPE = ::Object.new
|
96
87
|
# Call this method to provide a new subscription_scope; OR
|
97
88
|
# call it without an argument to get the subscription_scope
|
98
89
|
# @param new_scope [Symbol]
|
99
90
|
# @return [Symbol]
|
100
|
-
READING_SCOPE = ::Object.new
|
101
91
|
def self.subscription_scope(new_scope = READING_SCOPE)
|
102
92
|
if new_scope != READING_SCOPE
|
103
93
|
@subscription_scope = new_scope
|
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
|
@@ -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 = {}
|
@@ -91,7 +90,6 @@ module GraphQL
|
|
91
90
|
|
92
91
|
# @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists
|
93
92
|
def get_field(parent_type, field_name)
|
94
|
-
|
95
93
|
@visible_parent_fields ||= read_through do |type|
|
96
94
|
read_through do |f_name|
|
97
95
|
field_defn = @schema.get_field(type, f_name)
|
@@ -200,7 +198,7 @@ module GraphQL
|
|
200
198
|
if (iface_field_defn = interface_type.get_field(field_defn.graphql_name))
|
201
199
|
any_interface_has_field = true
|
202
200
|
|
203
|
-
if
|
201
|
+
if interfaces(type_defn).include?(interface_type) && visible_field?(interface_type, iface_field_defn)
|
204
202
|
any_interface_has_visible_field = true
|
205
203
|
end
|
206
204
|
end
|
@@ -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,
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "securerandom"
|
3
|
+
require "graphql/subscriptions/broadcast_analyzer"
|
3
4
|
require "graphql/subscriptions/event"
|
4
5
|
require "graphql/subscriptions/instrumentation"
|
5
6
|
require "graphql/subscriptions/serialize"
|
@@ -7,6 +8,7 @@ if defined?(ActionCable)
|
|
7
8
|
require "graphql/subscriptions/action_cable_subscriptions"
|
8
9
|
end
|
9
10
|
require "graphql/subscriptions/subscription_root"
|
11
|
+
require "graphql/subscriptions/default_subscription_resolve_extension"
|
10
12
|
|
11
13
|
module GraphQL
|
12
14
|
class Subscriptions
|
@@ -29,14 +31,25 @@ module GraphQL
|
|
29
31
|
defn.instrument(:field, instrumentation)
|
30
32
|
options[:schema] = schema
|
31
33
|
schema.subscriptions = self.new(**options)
|
34
|
+
schema.add_subscription_extension_if_necessary
|
32
35
|
nil
|
33
36
|
end
|
34
37
|
|
35
38
|
# @param schema [Class] the GraphQL schema this manager belongs to
|
36
|
-
def initialize(schema:, **rest)
|
39
|
+
def initialize(schema:, broadcast: false, default_broadcastable: false, **rest)
|
40
|
+
if broadcast
|
41
|
+
if !schema.using_ast_analysis?
|
42
|
+
raise ArgumentError, "`broadcast: true` requires AST analysis, add `using GraphQL::Analysis::AST` to your schema or see https://graphql-ruby.org/queries/ast_analysis.html."
|
43
|
+
end
|
44
|
+
schema.query_analyzer(Subscriptions::BroadcastAnalyzer)
|
45
|
+
end
|
46
|
+
@default_broadcastable = default_broadcastable
|
37
47
|
@schema = schema
|
38
48
|
end
|
39
49
|
|
50
|
+
# @return [Boolean] Used when fields don't have `broadcastable:` explicitly set
|
51
|
+
attr_reader :default_broadcastable
|
52
|
+
|
40
53
|
# Fetch subscriptions matching this field + arguments pair
|
41
54
|
# And pass them off to the queue.
|
42
55
|
# @param event_name [String]
|
@@ -77,42 +90,64 @@ module GraphQL
|
|
77
90
|
# `event` was triggered on `object`, and `subscription_id` was subscribed,
|
78
91
|
# so it should be updated.
|
79
92
|
#
|
80
|
-
# Load `subscription_id`'s GraphQL data, re-evaluate the query
|
81
|
-
#
|
82
|
-
# This is where a queue may be inserted to push updates in the background.
|
93
|
+
# Load `subscription_id`'s GraphQL data, re-evaluate the query and return the result.
|
83
94
|
#
|
84
95
|
# @param subscription_id [String]
|
85
96
|
# @param event [GraphQL::Subscriptions::Event] The event which was triggered
|
86
97
|
# @param object [Object] The value for the subscription field
|
87
|
-
# @return [
|
88
|
-
def
|
98
|
+
# @return [GraphQL::Query::Result]
|
99
|
+
def execute_update(subscription_id, event, object)
|
89
100
|
# Lookup the saved data for this subscription
|
90
101
|
query_data = read_subscription(subscription_id)
|
91
102
|
if query_data.nil?
|
92
|
-
|
93
|
-
|
103
|
+
delete_subscription(subscription_id)
|
104
|
+
return nil
|
94
105
|
end
|
106
|
+
|
95
107
|
# Fetch the required keys from the saved data
|
96
108
|
query_string = query_data.fetch(:query_string)
|
97
109
|
variables = query_data.fetch(:variables)
|
98
110
|
context = query_data.fetch(:context)
|
99
111
|
operation_name = query_data.fetch(:operation_name)
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
112
|
+
result = nil
|
113
|
+
# this will be set to `false` unless `.execute` is terminated
|
114
|
+
# with a `throw :graphql_subscription_unsubscribed`
|
115
|
+
unsubscribed = true
|
116
|
+
catch(:graphql_subscription_unsubscribed) do
|
117
|
+
catch(:graphql_no_subscription_update) do
|
118
|
+
# Re-evaluate the saved query,
|
119
|
+
# but if it terminates early with a `throw`,
|
120
|
+
# it will stay `nil`
|
121
|
+
result = @schema.execute(
|
122
|
+
query: query_string,
|
123
|
+
context: context,
|
124
|
+
subscription_topic: event.topic,
|
125
|
+
operation_name: operation_name,
|
126
|
+
variables: variables,
|
127
|
+
root_value: object,
|
128
|
+
)
|
129
|
+
end
|
130
|
+
unsubscribed = false
|
131
|
+
end
|
132
|
+
|
133
|
+
if unsubscribed
|
134
|
+
# `unsubscribe` was called, clean up on our side
|
135
|
+
# TODO also send `{more: false}` to client?
|
136
|
+
delete_subscription(subscription_id)
|
137
|
+
end
|
138
|
+
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
# Run the update query for this subscription and deliver it
|
143
|
+
# @see {#execute_update}
|
144
|
+
# @see {#deliver}
|
145
|
+
# @return [void]
|
146
|
+
def execute(subscription_id, event, object)
|
147
|
+
res = execute_update(subscription_id, event, object)
|
148
|
+
if !res.nil?
|
149
|
+
deliver(subscription_id, res)
|
150
|
+
end
|
116
151
|
end
|
117
152
|
|
118
153
|
# Event `event` occurred on `object`,
|
@@ -185,6 +220,16 @@ module GraphQL
|
|
185
220
|
Schema::Member::BuildType.camelize(event_or_arg_name.to_s)
|
186
221
|
end
|
187
222
|
|
223
|
+
# @return [Boolean] if true, then a query like this one would be broadcasted
|
224
|
+
def broadcastable?(query_str, **query_options)
|
225
|
+
query = GraphQL::Query.new(@schema, query_str, **query_options)
|
226
|
+
if !query.valid?
|
227
|
+
raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}"
|
228
|
+
end
|
229
|
+
GraphQL::Analysis::AST.analyze_query(query, @schema.query_analyzers)
|
230
|
+
query.context.namespace(:subscriptions)[:subscription_broadcastable]
|
231
|
+
end
|
232
|
+
|
188
233
|
private
|
189
234
|
|
190
235
|
# Recursively normalize `args` as belonging to `arg_owner`:
|