graphql 1.10.1 → 1.13.0
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.
- checksums.yaml +4 -4
- data/lib/generators/graphql/core.rb +18 -2
- data/lib/generators/graphql/install_generator.rb +36 -6
- data/lib/generators/graphql/loader_generator.rb +1 -0
- data/lib/generators/graphql/mutation_generator.rb +2 -1
- data/lib/generators/graphql/object_generator.rb +54 -9
- data/lib/generators/graphql/relay.rb +63 -0
- data/lib/generators/graphql/relay_generator.rb +21 -0
- data/lib/generators/graphql/templates/base_argument.erb +2 -0
- data/lib/generators/graphql/templates/base_connection.erb +8 -0
- data/lib/generators/graphql/templates/base_edge.erb +8 -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 +16 -12
- 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/node_type.erb +9 -0
- data/lib/generators/graphql/templates/object.erb +3 -1
- data/lib/generators/graphql/templates/query_type.erb +3 -3
- data/lib/generators/graphql/templates/scalar.erb +2 -0
- data/lib/generators/graphql/templates/schema.erb +21 -33
- data/lib/generators/graphql/templates/union.erb +3 -1
- data/lib/generators/graphql/type_generator.rb +1 -1
- data/lib/graphql/analysis/analyze_query.rb +7 -0
- data/lib/graphql/analysis/ast/field_usage.rb +24 -1
- data/lib/graphql/analysis/ast/query_complexity.rb +126 -109
- data/lib/graphql/analysis/ast/visitor.rb +13 -5
- data/lib/graphql/analysis/ast.rb +11 -2
- data/lib/graphql/argument.rb +3 -3
- data/lib/graphql/backtrace/inspect_result.rb +0 -1
- data/lib/graphql/backtrace/legacy_tracer.rb +56 -0
- data/lib/graphql/backtrace/table.rb +34 -3
- data/lib/graphql/backtrace/traced_error.rb +0 -1
- data/lib/graphql/backtrace/tracer.rb +40 -9
- data/lib/graphql/backtrace.rb +28 -19
- data/lib/graphql/backwards_compatibility.rb +2 -1
- data/lib/graphql/base_type.rb +1 -1
- data/lib/graphql/compatibility/execution_specification/specification_schema.rb +2 -2
- data/lib/graphql/compatibility/execution_specification.rb +1 -0
- data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
- data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
- data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
- data/lib/graphql/dataloader/null_dataloader.rb +22 -0
- data/lib/graphql/dataloader/request.rb +19 -0
- data/lib/graphql/dataloader/request_all.rb +19 -0
- data/lib/graphql/dataloader/source.rb +155 -0
- data/lib/graphql/dataloader.rb +308 -0
- data/lib/graphql/define/assign_global_id_field.rb +2 -2
- data/lib/graphql/define/defined_object_proxy.rb +1 -1
- data/lib/graphql/define/instance_definable.rb +34 -4
- data/lib/graphql/define/type_definer.rb +5 -5
- data/lib/graphql/deprecated_dsl.rb +18 -5
- data/lib/graphql/deprecation.rb +9 -0
- data/lib/graphql/directive.rb +4 -4
- data/lib/graphql/enum_type.rb +7 -1
- data/lib/graphql/execution/errors.rb +110 -7
- data/lib/graphql/execution/execute.rb +8 -1
- data/lib/graphql/execution/instrumentation.rb +1 -1
- data/lib/graphql/execution/interpreter/argument_value.rb +28 -0
- data/lib/graphql/execution/interpreter/arguments.rb +88 -0
- data/lib/graphql/execution/interpreter/arguments_cache.rb +103 -0
- data/lib/graphql/execution/interpreter/handles_raw_value.rb +18 -0
- data/lib/graphql/execution/interpreter/resolve.rb +37 -25
- data/lib/graphql/execution/interpreter/runtime.rb +685 -421
- data/lib/graphql/execution/interpreter.rb +42 -13
- data/lib/graphql/execution/lazy.rb +5 -1
- data/lib/graphql/execution/lookahead.rb +25 -110
- data/lib/graphql/execution/multiplex.rb +37 -25
- data/lib/graphql/field.rb +5 -1
- data/lib/graphql/function.rb +4 -0
- data/lib/graphql/input_object_type.rb +6 -0
- data/lib/graphql/integer_decoding_error.rb +17 -0
- data/lib/graphql/integer_encoding_error.rb +18 -2
- data/lib/graphql/interface_type.rb +7 -0
- data/lib/graphql/internal_representation/document.rb +2 -2
- data/lib/graphql/internal_representation/rewrite.rb +1 -1
- data/lib/graphql/internal_representation/scope.rb +2 -2
- data/lib/graphql/internal_representation/visit.rb +2 -2
- data/lib/graphql/introspection/directive_type.rb +8 -4
- data/lib/graphql/introspection/entry_points.rb +2 -2
- data/lib/graphql/introspection/enum_value_type.rb +2 -2
- data/lib/graphql/introspection/field_type.rb +9 -5
- data/lib/graphql/introspection/input_value_type.rb +15 -3
- data/lib/graphql/introspection/introspection_query.rb +6 -92
- data/lib/graphql/introspection/schema_type.rb +4 -4
- data/lib/graphql/introspection/type_type.rb +16 -12
- data/lib/graphql/introspection.rb +96 -0
- data/lib/graphql/invalid_null_error.rb +18 -0
- data/lib/graphql/language/block_string.rb +20 -5
- data/lib/graphql/language/cache.rb +37 -0
- data/lib/graphql/language/document_from_schema_definition.rb +73 -25
- data/lib/graphql/language/lexer.rb +4 -3
- data/lib/graphql/language/lexer.rl +3 -3
- data/lib/graphql/language/nodes.rb +51 -89
- data/lib/graphql/language/parser.rb +552 -530
- data/lib/graphql/language/parser.y +114 -99
- data/lib/graphql/language/printer.rb +7 -2
- data/lib/graphql/language/sanitized_printer.rb +222 -0
- data/lib/graphql/language/token.rb +0 -4
- data/lib/graphql/language/visitor.rb +2 -2
- data/lib/graphql/language.rb +2 -0
- data/lib/graphql/name_validator.rb +2 -7
- data/lib/graphql/object_type.rb +44 -35
- data/lib/graphql/pagination/active_record_relation_connection.rb +14 -1
- data/lib/graphql/pagination/array_connection.rb +2 -2
- data/lib/graphql/pagination/connection.rb +75 -20
- data/lib/graphql/pagination/connections.rb +83 -31
- data/lib/graphql/pagination/relation_connection.rb +34 -14
- data/lib/graphql/parse_error.rb +0 -1
- data/lib/graphql/query/arguments.rb +4 -3
- data/lib/graphql/query/arguments_cache.rb +1 -2
- data/lib/graphql/query/context.rb +42 -7
- data/lib/graphql/query/executor.rb +0 -1
- data/lib/graphql/query/fingerprint.rb +26 -0
- data/lib/graphql/query/input_validation_result.rb +23 -6
- data/lib/graphql/query/literal_input.rb +1 -1
- data/lib/graphql/query/null_context.rb +24 -8
- data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
- data/lib/graphql/query/serial_execution.rb +1 -0
- data/lib/graphql/query/validation_pipeline.rb +5 -2
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/query/variables.rb +14 -4
- data/lib/graphql/query.rb +68 -13
- data/lib/graphql/railtie.rb +9 -1
- data/lib/graphql/rake_task.rb +12 -9
- data/lib/graphql/relay/array_connection.rb +10 -12
- data/lib/graphql/relay/base_connection.rb +26 -13
- data/lib/graphql/relay/connection_instrumentation.rb +4 -4
- data/lib/graphql/relay/connection_type.rb +1 -1
- data/lib/graphql/relay/edges_instrumentation.rb +0 -1
- data/lib/graphql/relay/mutation.rb +1 -0
- data/lib/graphql/relay/node.rb +3 -0
- data/lib/graphql/relay/range_add.rb +23 -9
- data/lib/graphql/relay/relation_connection.rb +8 -10
- data/lib/graphql/relay/type_extensions.rb +2 -0
- data/lib/graphql/rubocop/graphql/base_cop.rb +36 -0
- data/lib/graphql/rubocop/graphql/default_null_true.rb +43 -0
- data/lib/graphql/rubocop/graphql/default_required_true.rb +43 -0
- data/lib/graphql/rubocop.rb +4 -0
- data/lib/graphql/scalar_type.rb +16 -1
- data/lib/graphql/schema/addition.rb +247 -0
- data/lib/graphql/schema/argument.rb +210 -12
- data/lib/graphql/schema/base_64_encoder.rb +2 -0
- data/lib/graphql/schema/build_from_definition/resolve_map.rb +3 -1
- data/lib/graphql/schema/build_from_definition.rb +213 -86
- data/lib/graphql/schema/default_type_error.rb +2 -0
- data/lib/graphql/schema/directive/deprecated.rb +1 -1
- data/lib/graphql/schema/directive/feature.rb +1 -1
- data/lib/graphql/schema/directive/flagged.rb +57 -0
- data/lib/graphql/schema/directive/include.rb +1 -1
- data/lib/graphql/schema/directive/skip.rb +1 -1
- data/lib/graphql/schema/directive/transform.rb +14 -2
- data/lib/graphql/schema/directive.rb +78 -2
- data/lib/graphql/schema/enum.rb +80 -9
- data/lib/graphql/schema/enum_value.rb +17 -6
- data/lib/graphql/schema/field/connection_extension.rb +46 -30
- data/lib/graphql/schema/field/scope_extension.rb +1 -1
- data/lib/graphql/schema/field.rb +285 -133
- data/lib/graphql/schema/find_inherited_value.rb +4 -1
- data/lib/graphql/schema/finder.rb +5 -5
- data/lib/graphql/schema/input_object.rb +97 -89
- data/lib/graphql/schema/interface.rb +24 -19
- data/lib/graphql/schema/late_bound_type.rb +2 -2
- data/lib/graphql/schema/list.rb +7 -1
- data/lib/graphql/schema/loader.rb +137 -103
- data/lib/graphql/schema/member/accepts_definition.rb +8 -1
- data/lib/graphql/schema/member/base_dsl_methods.rb +15 -19
- data/lib/graphql/schema/member/build_type.rb +14 -7
- data/lib/graphql/schema/member/has_arguments.rb +205 -12
- data/lib/graphql/schema/member/has_ast_node.rb +4 -1
- data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
- data/lib/graphql/schema/member/has_directives.rb +98 -0
- data/lib/graphql/schema/member/has_fields.rb +95 -30
- data/lib/graphql/schema/member/has_interfaces.rb +90 -0
- data/lib/graphql/schema/member/has_unresolved_type_error.rb +15 -0
- data/lib/graphql/schema/member/has_validators.rb +31 -0
- data/lib/graphql/schema/member/instrumentation.rb +0 -1
- data/lib/graphql/schema/member/type_system_helpers.rb +3 -3
- data/lib/graphql/schema/member.rb +6 -0
- data/lib/graphql/schema/middleware_chain.rb +1 -1
- data/lib/graphql/schema/mutation.rb +4 -0
- data/lib/graphql/schema/non_null.rb +5 -0
- data/lib/graphql/schema/object.rb +47 -46
- data/lib/graphql/schema/possible_types.rb +9 -4
- data/lib/graphql/schema/printer.rb +16 -34
- data/lib/graphql/schema/relay_classic_mutation.rb +32 -4
- data/lib/graphql/schema/resolver/has_payload_type.rb +34 -4
- data/lib/graphql/schema/resolver.rb +123 -63
- data/lib/graphql/schema/scalar.rb +11 -1
- data/lib/graphql/schema/subscription.rb +57 -21
- data/lib/graphql/schema/timeout.rb +29 -15
- data/lib/graphql/schema/timeout_middleware.rb +3 -1
- data/lib/graphql/schema/type_expression.rb +1 -1
- data/lib/graphql/schema/type_membership.rb +18 -4
- data/lib/graphql/schema/union.rb +41 -1
- data/lib/graphql/schema/unique_within_type.rb +1 -2
- data/lib/graphql/schema/validation.rb +12 -2
- data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
- data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
- data/lib/graphql/schema/validator/exclusion_validator.rb +33 -0
- data/lib/graphql/schema/validator/format_validator.rb +48 -0
- data/lib/graphql/schema/validator/inclusion_validator.rb +35 -0
- data/lib/graphql/schema/validator/length_validator.rb +59 -0
- data/lib/graphql/schema/validator/numericality_validator.rb +82 -0
- data/lib/graphql/schema/validator/required_validator.rb +68 -0
- data/lib/graphql/schema/validator.rb +174 -0
- data/lib/graphql/schema/warden.rb +153 -28
- data/lib/graphql/schema.rb +364 -330
- data/lib/graphql/static_validation/all_rules.rb +1 -0
- data/lib/graphql/static_validation/base_visitor.rb +8 -5
- data/lib/graphql/static_validation/definition_dependencies.rb +0 -1
- data/lib/graphql/static_validation/error.rb +3 -1
- data/lib/graphql/static_validation/literal_validator.rb +51 -26
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +44 -87
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +22 -6
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +28 -22
- data/lib/graphql/static_validation/rules/arguments_are_defined_error.rb +4 -2
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +79 -43
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
- data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
- data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
- data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +6 -7
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +9 -10
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +8 -8
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +4 -2
- data/lib/graphql/static_validation/validation_context.rb +9 -3
- data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
- data/lib/graphql/static_validation/validator.rb +42 -8
- data/lib/graphql/static_validation.rb +1 -0
- data/lib/graphql/string_encoding_error.rb +13 -3
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +118 -19
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +81 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
- data/lib/graphql/subscriptions/event.rb +81 -30
- data/lib/graphql/subscriptions/instrumentation.rb +0 -1
- data/lib/graphql/subscriptions/serialize.rb +33 -6
- data/lib/graphql/subscriptions/subscription_root.rb +15 -4
- data/lib/graphql/subscriptions.rb +88 -45
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +2 -1
- data/lib/graphql/tracing/appoptics_tracing.rb +173 -0
- data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
- data/lib/graphql/tracing/new_relic_tracing.rb +1 -12
- data/lib/graphql/tracing/platform_tracing.rb +43 -17
- data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
- data/lib/graphql/tracing/scout_tracing.rb +11 -0
- data/lib/graphql/tracing/skylight_tracing.rb +1 -1
- data/lib/graphql/tracing/statsd_tracing.rb +42 -0
- data/lib/graphql/tracing.rb +9 -33
- data/lib/graphql/types/big_int.rb +5 -1
- data/lib/graphql/types/int.rb +10 -3
- data/lib/graphql/types/iso_8601_date.rb +3 -3
- data/lib/graphql/types/iso_8601_date_time.rb +25 -10
- data/lib/graphql/types/relay/base_connection.rb +6 -90
- data/lib/graphql/types/relay/base_edge.rb +2 -34
- data/lib/graphql/types/relay/connection_behaviors.rb +156 -0
- data/lib/graphql/types/relay/default_relay.rb +27 -0
- data/lib/graphql/types/relay/edge_behaviors.rb +53 -0
- data/lib/graphql/types/relay/has_node_field.rb +41 -0
- data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
- data/lib/graphql/types/relay/node.rb +2 -4
- data/lib/graphql/types/relay/node_behaviors.rb +15 -0
- data/lib/graphql/types/relay/node_field.rb +2 -20
- data/lib/graphql/types/relay/nodes_field.rb +2 -20
- data/lib/graphql/types/relay/page_info.rb +2 -14
- data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
- data/lib/graphql/types/relay.rb +11 -3
- data/lib/graphql/types/string.rb +8 -2
- data/lib/graphql/unauthorized_error.rb +2 -2
- data/lib/graphql/union_type.rb +2 -0
- data/lib/graphql/upgrader/member.rb +1 -0
- data/lib/graphql/upgrader/schema.rb +1 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +65 -31
- data/readme.md +3 -6
- metadata +77 -112
- data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
- data/lib/graphql/literal_validation_error.rb +0 -6
- data/lib/graphql/types/relay/base_field.rb +0 -22
- data/lib/graphql/types/relay/base_interface.rb +0 -29
- data/lib/graphql/types/relay/base_object.rb +0 -26
@@ -4,10 +4,11 @@ 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?})
|
11
|
+
# - Avoid the async ActionCable adapter and use the redis or PostgreSQL adapters instead. Otherwise calling #trigger won't work from background jobs or the Rails console.
|
11
12
|
#
|
12
13
|
# @example Adding ActionCableSubscriptions to your schema
|
13
14
|
# class MySchema < GraphQL::Schema
|
@@ -33,12 +34,12 @@ module GraphQL
|
|
33
34
|
# channel: self,
|
34
35
|
# }
|
35
36
|
#
|
36
|
-
# result = MySchema.execute(
|
37
|
+
# result = MySchema.execute(
|
37
38
|
# query: query,
|
38
39
|
# context: context,
|
39
40
|
# variables: variables,
|
40
41
|
# operation_name: operation_name
|
41
|
-
#
|
42
|
+
# )
|
42
43
|
#
|
43
44
|
# payload = {
|
44
45
|
# result: result.to_h,
|
@@ -85,27 +86,40 @@ module GraphQL
|
|
85
86
|
EVENT_PREFIX = "graphql-event:"
|
86
87
|
|
87
88
|
# @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
|
88
|
-
|
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)
|
89
91
|
# A per-process map of subscriptions to deliver.
|
90
92
|
# This is provided by Rails, so let's use it
|
91
93
|
@subscriptions = Concurrent::Map.new
|
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
|
92
97
|
@serializer = serializer
|
98
|
+
@serialize_with_context = case @serializer.method(:load).arity
|
99
|
+
when 1
|
100
|
+
false
|
101
|
+
when 2
|
102
|
+
true
|
103
|
+
else
|
104
|
+
raise ArgumentError, "#{@serializer} must repond to `.load` accepting one or two arguments"
|
105
|
+
end
|
106
|
+
@transmit_ns = namespace
|
93
107
|
super
|
94
108
|
end
|
95
109
|
|
96
110
|
# An event was triggered; Push the data over ActionCable.
|
97
111
|
# Subscribers will re-evaluate locally.
|
98
112
|
def execute_all(event, object)
|
99
|
-
stream =
|
113
|
+
stream = stream_event_name(event)
|
100
114
|
message = @serializer.dump(object)
|
101
|
-
|
115
|
+
@action_cable.server.broadcast(stream, message)
|
102
116
|
end
|
103
117
|
|
104
118
|
# This subscription was re-evaluated.
|
105
119
|
# Send it to the specific stream where this client was waiting.
|
106
120
|
def deliver(subscription_id, result)
|
107
121
|
payload = { result: result.to_h, more: true }
|
108
|
-
|
122
|
+
@action_cable.server.broadcast(stream_subscription_name(subscription_id), payload)
|
109
123
|
end
|
110
124
|
|
111
125
|
# A query was run where these events were subscribed to.
|
@@ -113,33 +127,118 @@ module GraphQL
|
|
113
127
|
# It will receive notifications when events come in
|
114
128
|
# and re-evaluate the query locally.
|
115
129
|
def write_subscription(query, events)
|
116
|
-
channel = query.context
|
130
|
+
unless (channel = query.context[:channel])
|
131
|
+
raise GraphQL::Error, "This GraphQL Subscription client does not support the transport protocol expected"\
|
132
|
+
"by the backend Subscription Server implementation (graphql-ruby ActionCableSubscriptions in this case)."\
|
133
|
+
"Some official client implementation including Apollo (https://graphql-ruby.org/javascript_client/apollo_subscriptions.html), "\
|
134
|
+
"Relay Modern (https://graphql-ruby.org/javascript_client/relay_subscriptions.html#actioncable)."\
|
135
|
+
"GraphiQL via `graphiql-rails` may not work out of box (#1051)."
|
136
|
+
end
|
117
137
|
subscription_id = query.context[:subscription_id] ||= build_id
|
118
|
-
stream =
|
138
|
+
stream = stream_subscription_name(subscription_id)
|
119
139
|
channel.stream_from(stream)
|
120
140
|
@subscriptions[subscription_id] = query
|
121
141
|
events.each do |event|
|
122
|
-
|
123
|
-
|
124
|
-
|
142
|
+
# Setup a new listener to run all events with this topic in this process
|
143
|
+
setup_stream(channel, event)
|
144
|
+
# Add this event to the list of events to be updated
|
145
|
+
@events[event.topic][event.fingerprint] << event
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Every subscribing channel is listening here, but only one of them takes any action.
|
150
|
+
# This is so we can reuse payloads when possible, and make one payload to send to
|
151
|
+
# all subscribers.
|
152
|
+
#
|
153
|
+
# But the problem is, any channel could close at any time, so each channel has to
|
154
|
+
# be ready to take over the primary position.
|
155
|
+
#
|
156
|
+
# To make sure there's always one-and-only-one channel building payloads,
|
157
|
+
# let the listener belonging to the first event on the list be
|
158
|
+
# the one to build and publish payloads.
|
159
|
+
#
|
160
|
+
def setup_stream(channel, initial_event)
|
161
|
+
topic = initial_event.topic
|
162
|
+
channel.stream_from(stream_event_name(initial_event), coder: @action_cable_coder) do |message|
|
163
|
+
events_by_fingerprint = @events[topic]
|
164
|
+
object = nil
|
165
|
+
events_by_fingerprint.each do |_fingerprint, events|
|
166
|
+
if events.any? && events.first == initial_event
|
167
|
+
# The fingerprint has told us that this response should be shared by all subscribers,
|
168
|
+
# so just run it once, then deliver the result to every subscriber
|
169
|
+
first_event = events.first
|
170
|
+
first_subscription_id = first_event.context.fetch(:subscription_id)
|
171
|
+
object ||= load_action_cable_message(message, first_event.context)
|
172
|
+
result = execute_update(first_subscription_id, first_event, object)
|
173
|
+
if !result.nil?
|
174
|
+
# Having calculated the result _once_, send the same payload to all subscribers
|
175
|
+
events.each do |event|
|
176
|
+
subscription_id = event.context.fetch(:subscription_id)
|
177
|
+
deliver(subscription_id, result)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
125
181
|
end
|
182
|
+
nil
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# This is called to turn an ActionCable-broadcasted string (JSON)
|
187
|
+
# into a query-ready application object.
|
188
|
+
# @param message [String] n ActionCable-broadcasted string (JSON)
|
189
|
+
# @param context [GraphQL::Query::Context] the context of the first event for a given subscription fingerprint
|
190
|
+
def load_action_cable_message(message, context)
|
191
|
+
if @serialize_with_context
|
192
|
+
@serializer.load(message, context)
|
193
|
+
else
|
194
|
+
@serializer.load(message)
|
126
195
|
end
|
127
196
|
end
|
128
197
|
|
129
198
|
# Return the query from "storage" (in memory)
|
130
199
|
def read_subscription(subscription_id)
|
131
200
|
query = @subscriptions[subscription_id]
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
201
|
+
if query.nil?
|
202
|
+
# This can happen when a subscription is triggered from an unsubscribed channel,
|
203
|
+
# see https://github.com/rmosolgo/graphql-ruby/issues/2478.
|
204
|
+
# (This `nil` is handled by `#execute_update`)
|
205
|
+
nil
|
206
|
+
else
|
207
|
+
{
|
208
|
+
query_string: query.query_string,
|
209
|
+
variables: query.provided_variables,
|
210
|
+
context: query.context.to_h,
|
211
|
+
operation_name: query.operation_name,
|
212
|
+
}
|
213
|
+
end
|
138
214
|
end
|
139
215
|
|
140
216
|
# The channel was closed, forget about it.
|
141
217
|
def delete_subscription(subscription_id)
|
142
|
-
@subscriptions.delete(subscription_id)
|
218
|
+
query = @subscriptions.delete(subscription_id)
|
219
|
+
# This can be `nil` when `.trigger` happens inside an unsubscribed ActionCable channel,
|
220
|
+
# see https://github.com/rmosolgo/graphql-ruby/issues/2478
|
221
|
+
if query
|
222
|
+
events = query.context.namespace(:subscriptions)[:events]
|
223
|
+
events.each do |event|
|
224
|
+
ev_by_fingerprint = @events[event.topic]
|
225
|
+
ev_for_fingerprint = ev_by_fingerprint[event.fingerprint]
|
226
|
+
ev_for_fingerprint.delete(event)
|
227
|
+
if ev_for_fingerprint.empty?
|
228
|
+
ev_by_fingerprint.delete(event.fingerprint)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
def stream_subscription_name(subscription_id)
|
237
|
+
[SUBSCRIPTION_PREFIX, @transmit_ns, subscription_id].join
|
238
|
+
end
|
239
|
+
|
240
|
+
def stream_event_name(event)
|
241
|
+
[EVENT_PREFIX, @transmit_ns, event.topic].join
|
143
242
|
end
|
144
243
|
end
|
145
244
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Subscriptions
|
5
|
+
# Detect whether the current operation:
|
6
|
+
# - Is a subscription operation
|
7
|
+
# - Is completely broadcastable
|
8
|
+
#
|
9
|
+
# Assign the result to `context.namespace(:subscriptions)[:subscription_broadcastable]`
|
10
|
+
# @api private
|
11
|
+
# @see Subscriptions#broadcastable? for a public API
|
12
|
+
class BroadcastAnalyzer < GraphQL::Analysis::AST::Analyzer
|
13
|
+
def initialize(subject)
|
14
|
+
super
|
15
|
+
@default_broadcastable = subject.schema.subscriptions.default_broadcastable
|
16
|
+
# Maybe this will get set to false while analyzing
|
17
|
+
@subscription_broadcastable = true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Only analyze subscription operations
|
21
|
+
def analyze?
|
22
|
+
@query.subscription?
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_enter_field(node, parent, visitor)
|
26
|
+
if (@subscription_broadcastable == false) || visitor.skipping?
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
current_field = visitor.field_definition
|
31
|
+
apply_broadcastable(current_field)
|
32
|
+
|
33
|
+
current_type = visitor.parent_type_definition
|
34
|
+
if current_type.kind.interface?
|
35
|
+
pt = @query.possible_types(current_type)
|
36
|
+
pt.each do |object_type|
|
37
|
+
ot_field = @query.get_field(object_type, current_field.graphql_name)
|
38
|
+
# Inherited fields would be exactly the same object;
|
39
|
+
# only check fields that are overrides of the inherited one
|
40
|
+
if ot_field && ot_field != current_field
|
41
|
+
apply_broadcastable(ot_field)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Assign the result to context.
|
48
|
+
# (This method is allowed to return an error, but we don't need to)
|
49
|
+
# @return [void]
|
50
|
+
def result
|
51
|
+
query.context.namespace(:subscriptions)[:subscription_broadcastable] = @subscription_broadcastable
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Modify `@subscription_broadcastable` based on `field_defn`'s configuration (and/or the default value)
|
58
|
+
def apply_broadcastable(field_defn)
|
59
|
+
current_field_broadcastable = field_defn.introspection? || field_defn.broadcastable?
|
60
|
+
case current_field_broadcastable
|
61
|
+
when nil
|
62
|
+
# If the value wasn't set, mix in the default value:
|
63
|
+
# - If the default is false and the current value is true, make it false
|
64
|
+
# - If the default is true and the current value is true, it stays true
|
65
|
+
# - If the default is false and the current value is false, keep it false
|
66
|
+
# - If the default is true and the current value is false, keep it false
|
67
|
+
@subscription_broadcastable = @subscription_broadcastable && @default_broadcastable
|
68
|
+
when false
|
69
|
+
# One non-broadcastable field is enough to make the whole subscription non-broadcastable
|
70
|
+
@subscription_broadcastable = false
|
71
|
+
when true
|
72
|
+
# Leave `@broadcastable_query` true if it's already true,
|
73
|
+
# but don't _set_ it to true if it was set to false by something else.
|
74
|
+
# Actually, just leave it!
|
75
|
+
else
|
76
|
+
raise ArgumentError, "Unexpected `.broadcastable?` value for #{field_defn.path}: #{current_field_broadcastable}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Subscriptions
|
4
|
+
class DefaultSubscriptionResolveExtension < GraphQL::Subscriptions::SubscriptionRoot::Extension
|
5
|
+
def resolve(context:, object:, arguments:)
|
6
|
+
has_override_implementation = @field.resolver ||
|
7
|
+
object.respond_to?(@field.resolver_method)
|
8
|
+
|
9
|
+
if !has_override_implementation
|
10
|
+
if context.query.subscription_update?
|
11
|
+
object.object
|
12
|
+
else
|
13
|
+
context.skip
|
14
|
+
end
|
15
|
+
else
|
16
|
+
yield(object, arguments)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,12 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# test_via: ../subscriptions.rb
|
3
2
|
module GraphQL
|
4
3
|
class Subscriptions
|
5
4
|
# This thing can be:
|
6
5
|
# - Subscribed to by `subscription { ... }`
|
7
6
|
# - Triggered by `MySchema.subscriber.trigger(name, arguments, obj)`
|
8
7
|
#
|
9
|
-
# An array of `Event`s are passed to `store.register(query, events)`.
|
10
8
|
class Event
|
11
9
|
# @return [String] Corresponds to the Subscription root field name
|
12
10
|
attr_reader :name
|
@@ -25,67 +23,120 @@ module GraphQL
|
|
25
23
|
@arguments = arguments
|
26
24
|
@context = context
|
27
25
|
field ||= context.field
|
28
|
-
|
26
|
+
scope_key = field.subscription_scope
|
27
|
+
scope_val = scope || (context && scope_key && context[scope_key])
|
28
|
+
if scope_key &&
|
29
|
+
(subscription = field.resolver) &&
|
30
|
+
(subscription.respond_to?(:subscription_scope_optional?)) &&
|
31
|
+
!subscription.subscription_scope_optional? &&
|
32
|
+
scope_val.nil?
|
33
|
+
raise Subscriptions::SubscriptionScopeMissingError, "#{field.path} (#{subscription}) requires a `scope:` value to trigger updates (Set `subscription_scope ..., optional: true` to disable this requirement)"
|
34
|
+
end
|
29
35
|
|
30
|
-
@topic = self.class.serialize(name, arguments, field, scope: scope_val)
|
36
|
+
@topic = self.class.serialize(name, arguments, field, scope: scope_val, context: context)
|
31
37
|
end
|
32
38
|
|
33
39
|
# @return [String] an identifier for this unit of subscription
|
34
|
-
def self.serialize(
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
def self.serialize(_name, arguments, field, scope:, context: GraphQL::Query::NullContext)
|
41
|
+
subscription = field.resolver || GraphQL::Schema::Subscription
|
42
|
+
normalized_args = stringify_args(field, arguments.to_h, context)
|
43
|
+
subscription.topic_for(arguments: normalized_args, field: field, scope: scope)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String] a logical identifier for this event. (Stable when the query is broadcastable.)
|
47
|
+
def fingerprint
|
48
|
+
@fingerprint ||= begin
|
49
|
+
# When this query has been flagged as broadcastable,
|
50
|
+
# use a generalized, stable fingerprint so that
|
51
|
+
# duplicate subscriptions can be evaluated and distributed in bulk.
|
52
|
+
# (`@topic` includes field, args, and subscription scope already.)
|
53
|
+
if @context.namespace(:subscriptions)[:subscription_broadcastable]
|
54
|
+
"#{@topic}/#{@context.query.fingerprint}"
|
41
55
|
else
|
42
|
-
|
43
|
-
|
44
|
-
field,
|
45
|
-
nil,
|
46
|
-
)
|
56
|
+
# not broadcastable, build a unique ID for this event
|
57
|
+
@context.schema.subscriptions.build_id
|
47
58
|
end
|
48
|
-
else
|
49
|
-
raise ArgumentError, "Unexpected arguments: #{arguments}, must be Hash or GraphQL::Arguments"
|
50
59
|
end
|
51
|
-
|
52
|
-
sorted_h = stringify_args(field, normalized_args.to_h)
|
53
|
-
Serialize.dump_recursive([scope, name, sorted_h])
|
54
60
|
end
|
55
61
|
|
56
62
|
class << self
|
57
63
|
private
|
58
|
-
|
64
|
+
|
65
|
+
# This method does not support cyclic references in the Hash,
|
66
|
+
# nor does it support Hashes whose keys are not sortable
|
67
|
+
# with respect to their peers ( cases where a <=> b might throw an error )
|
68
|
+
def deep_sort_hash_keys(hash_to_sort)
|
69
|
+
raise ArgumentError.new("Argument must be a Hash") unless hash_to_sort.is_a?(Hash)
|
70
|
+
hash_to_sort.keys.sort.map do |k|
|
71
|
+
if hash_to_sort[k].is_a?(Hash)
|
72
|
+
[k, deep_sort_hash_keys(hash_to_sort[k])]
|
73
|
+
elsif hash_to_sort[k].is_a?(Array)
|
74
|
+
[k, deep_sort_array_hashes(hash_to_sort[k])]
|
75
|
+
else
|
76
|
+
[k, hash_to_sort[k]]
|
77
|
+
end
|
78
|
+
end.to_h
|
79
|
+
end
|
80
|
+
|
81
|
+
def deep_sort_array_hashes(array_to_inspect)
|
82
|
+
raise ArgumentError.new("Argument must be an Array") unless array_to_inspect.is_a?(Array)
|
83
|
+
array_to_inspect.map do |v|
|
84
|
+
if v.is_a?(Hash)
|
85
|
+
deep_sort_hash_keys(v)
|
86
|
+
elsif v.is_a?(Array)
|
87
|
+
deep_sort_array_hashes(v)
|
88
|
+
else
|
89
|
+
v
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def stringify_args(arg_owner, args, context)
|
95
|
+
arg_owner = arg_owner.respond_to?(:unwrap) ? arg_owner.unwrap : arg_owner # remove list and non-null wrappers
|
59
96
|
case args
|
60
97
|
when Hash
|
61
98
|
next_args = {}
|
62
99
|
args.each do |k, v|
|
63
100
|
arg_name = k.to_s
|
64
101
|
camelized_arg_name = GraphQL::Schema::Member::BuildType.camelize(arg_name)
|
65
|
-
arg_defn = get_arg_definition(arg_owner, camelized_arg_name)
|
102
|
+
arg_defn = get_arg_definition(arg_owner, camelized_arg_name, context)
|
66
103
|
|
67
104
|
if arg_defn
|
68
105
|
normalized_arg_name = camelized_arg_name
|
69
106
|
else
|
70
107
|
normalized_arg_name = arg_name
|
71
|
-
arg_defn = get_arg_definition(arg_owner, normalized_arg_name)
|
108
|
+
arg_defn = get_arg_definition(arg_owner, normalized_arg_name, context)
|
109
|
+
end
|
110
|
+
arg_base_type = arg_defn.type.unwrap
|
111
|
+
# In the case where the value being emitted is seen as a "JSON"
|
112
|
+
# type, treat the value as one atomic unit of serialization
|
113
|
+
is_json_definition = arg_base_type && arg_base_type <= GraphQL::Types::JSON
|
114
|
+
if is_json_definition
|
115
|
+
sorted_value = if v.is_a?(Hash)
|
116
|
+
deep_sort_hash_keys(v)
|
117
|
+
elsif v.is_a?(Array)
|
118
|
+
deep_sort_array_hashes(v)
|
119
|
+
else
|
120
|
+
v
|
121
|
+
end
|
122
|
+
next_args[normalized_arg_name] = sorted_value.respond_to?(:to_json) ? sorted_value.to_json : sorted_value
|
123
|
+
else
|
124
|
+
next_args[normalized_arg_name] = stringify_args(arg_base_type, v, context)
|
72
125
|
end
|
73
|
-
|
74
|
-
next_args[normalized_arg_name] = stringify_args(arg_defn.type, v)
|
75
126
|
end
|
76
127
|
# Make sure they're deeply sorted
|
77
128
|
next_args.sort.to_h
|
78
129
|
when Array
|
79
|
-
args.map { |a| stringify_args(arg_owner, a) }
|
130
|
+
args.map { |a| stringify_args(arg_owner, a, context) }
|
80
131
|
when GraphQL::Schema::InputObject
|
81
|
-
stringify_args(arg_owner, args.to_h)
|
132
|
+
stringify_args(arg_owner, args.to_h, context)
|
82
133
|
else
|
83
134
|
args
|
84
135
|
end
|
85
136
|
end
|
86
137
|
|
87
|
-
def get_arg_definition(arg_owner, arg_name)
|
88
|
-
arg_owner.
|
138
|
+
def get_arg_definition(arg_owner, arg_name, context)
|
139
|
+
arg_owner.get_argument(arg_name, context) || arg_owner.arguments(context).each_value.find { |v| v.keyword.to_s == arg_name }
|
89
140
|
end
|
90
141
|
end
|
91
142
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# test_via: ../subscriptions.rb
|
3
2
|
require "set"
|
4
3
|
module GraphQL
|
5
4
|
class Subscriptions
|
@@ -9,6 +8,9 @@ module GraphQL
|
|
9
8
|
GLOBALID_KEY = "__gid__"
|
10
9
|
SYMBOL_KEY = "__sym__"
|
11
10
|
SYMBOL_KEYS_KEY = "__sym_keys__"
|
11
|
+
TIMESTAMP_KEY = "__timestamp__"
|
12
|
+
TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%z" # eg '2020-01-01 23:59:59.123456789+05:00'
|
13
|
+
OPEN_STRUCT_KEY = "__ostruct__"
|
12
14
|
|
13
15
|
module_function
|
14
16
|
|
@@ -53,12 +55,32 @@ module GraphQL
|
|
53
55
|
# @return [Object] An object that load Global::Identification recursive
|
54
56
|
def load_value(value)
|
55
57
|
if value.is_a?(Array)
|
56
|
-
value.
|
58
|
+
is_gids = (v1 = value[0]).is_a?(Hash) && v1.size == 1 && v1[GLOBALID_KEY]
|
59
|
+
if is_gids
|
60
|
+
# Assume it's an array of global IDs
|
61
|
+
ids = value.map { |v| v[GLOBALID_KEY] }
|
62
|
+
GlobalID::Locator.locate_many(ids)
|
63
|
+
else
|
64
|
+
value.map { |item| load_value(item) }
|
65
|
+
end
|
57
66
|
elsif value.is_a?(Hash)
|
58
|
-
if value.size == 1
|
59
|
-
|
60
|
-
|
61
|
-
|
67
|
+
if value.size == 1
|
68
|
+
case value.keys.first # there's only 1 key
|
69
|
+
when GLOBALID_KEY
|
70
|
+
GlobalID::Locator.locate(value[GLOBALID_KEY])
|
71
|
+
when SYMBOL_KEY
|
72
|
+
value[SYMBOL_KEY].to_sym
|
73
|
+
when TIMESTAMP_KEY
|
74
|
+
timestamp_class_name, timestamp_s = value[TIMESTAMP_KEY]
|
75
|
+
timestamp_class = Object.const_get(timestamp_class_name)
|
76
|
+
timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT)
|
77
|
+
when OPEN_STRUCT_KEY
|
78
|
+
ostruct_values = load_value(value[OPEN_STRUCT_KEY])
|
79
|
+
OpenStruct.new(ostruct_values)
|
80
|
+
else
|
81
|
+
key = value.keys.first
|
82
|
+
{ key => load_value(value[key]) }
|
83
|
+
end
|
62
84
|
else
|
63
85
|
loaded_h = {}
|
64
86
|
sym_keys = value.fetch(SYMBOL_KEYS_KEY, [])
|
@@ -101,6 +123,11 @@ module GraphQL
|
|
101
123
|
{ SYMBOL_KEY => obj.to_s }
|
102
124
|
elsif obj.respond_to?(:to_gid_param)
|
103
125
|
{GLOBALID_KEY => obj.to_gid_param}
|
126
|
+
elsif obj.is_a?(Date) || obj.is_a?(Time)
|
127
|
+
# DateTime extends Date; for TimeWithZone, call `.utc` first.
|
128
|
+
{ TIMESTAMP_KEY => [obj.class.name, obj.strftime(TIMESTAMP_FORMAT)] }
|
129
|
+
elsif obj.is_a?(OpenStruct)
|
130
|
+
{ OPEN_STRUCT_KEY => dump_value(obj.to_h) }
|
104
131
|
else
|
105
132
|
obj
|
106
133
|
end
|
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
module GraphQL
|
4
4
|
class Subscriptions
|
5
|
-
#
|
5
|
+
# @api private
|
6
|
+
# @deprecated This module is no longer needed.
|
6
7
|
module SubscriptionRoot
|
7
8
|
def self.extended(child_cls)
|
9
|
+
GraphQL::Deprecation.warn "`extend GraphQL::Subscriptions::SubscriptionRoot` is no longer required; you can remove it from your Subscription type (#{child_cls})"
|
8
10
|
child_cls.include(InstanceMethods)
|
9
11
|
end
|
10
12
|
|
@@ -38,16 +40,17 @@ module GraphQL
|
|
38
40
|
elsif (events = context.namespace(:subscriptions)[:events])
|
39
41
|
# This is the first execution, so gather an Event
|
40
42
|
# for the backend to register:
|
41
|
-
|
43
|
+
event = Subscriptions::Event.new(
|
42
44
|
name: field.name,
|
43
|
-
arguments: arguments,
|
45
|
+
arguments: arguments_without_field_extras(arguments: arguments),
|
44
46
|
context: context,
|
45
47
|
field: field,
|
46
48
|
)
|
49
|
+
events << event
|
47
50
|
value
|
48
51
|
elsif context.query.subscription_topic == Subscriptions::Event.serialize(
|
49
52
|
field.name,
|
50
|
-
arguments,
|
53
|
+
arguments_without_field_extras(arguments: arguments),
|
51
54
|
field,
|
52
55
|
scope: (field.subscription_scope ? context[field.subscription_scope] : nil),
|
53
56
|
)
|
@@ -59,6 +62,14 @@ module GraphQL
|
|
59
62
|
context.skip
|
60
63
|
end
|
61
64
|
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def arguments_without_field_extras(arguments:)
|
69
|
+
arguments.dup.tap do |event_args|
|
70
|
+
field.extras.each { |k| event_args.delete(k) }
|
71
|
+
end
|
72
|
+
end
|
62
73
|
end
|
63
74
|
end
|
64
75
|
end
|