graphql 2.2.17 → 2.5.16
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/install/mutation_root_generator.rb +2 -2
- data/lib/generators/graphql/install_generator.rb +46 -0
- data/lib/generators/graphql/orm_mutations_base.rb +1 -1
- data/lib/generators/graphql/templates/base_resolver.erb +2 -0
- data/lib/generators/graphql/templates/schema.erb +3 -0
- data/lib/generators/graphql/type_generator.rb +1 -1
- data/lib/graphql/analysis/analyzer.rb +90 -0
- data/lib/graphql/analysis/field_usage.rb +82 -0
- data/lib/graphql/analysis/max_query_complexity.rb +20 -0
- data/lib/graphql/analysis/max_query_depth.rb +20 -0
- data/lib/graphql/analysis/query_complexity.rb +263 -0
- data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
- data/lib/graphql/analysis/visitor.rb +280 -0
- data/lib/graphql/analysis.rb +95 -1
- data/lib/graphql/autoload.rb +38 -0
- data/lib/graphql/backtrace/table.rb +118 -55
- data/lib/graphql/backtrace.rb +1 -19
- data/lib/graphql/current.rb +57 -0
- data/lib/graphql/dashboard/detailed_traces.rb +47 -0
- data/lib/graphql/dashboard/installable.rb +22 -0
- data/lib/graphql/dashboard/limiters.rb +93 -0
- data/lib/graphql/dashboard/operation_store.rb +199 -0
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
- data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
- data/lib/graphql/dashboard/statics/charts.min.css +1 -0
- data/lib/graphql/dashboard/statics/dashboard.css +30 -0
- data/lib/graphql/dashboard/statics/dashboard.js +143 -0
- data/lib/graphql/dashboard/statics/header-icon.png +0 -0
- data/lib/graphql/dashboard/statics/icon.png +0 -0
- data/lib/graphql/dashboard/subscriptions.rb +96 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +108 -0
- data/lib/graphql/dashboard.rb +158 -0
- data/lib/graphql/dataloader/active_record_association_source.rb +84 -0
- data/lib/graphql/dataloader/active_record_source.rb +47 -0
- data/lib/graphql/dataloader/async_dataloader.rb +46 -19
- data/lib/graphql/dataloader/null_dataloader.rb +51 -10
- data/lib/graphql/dataloader/source.rb +20 -9
- data/lib/graphql/dataloader.rb +153 -45
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/dig.rb +2 -1
- data/lib/graphql/execution/interpreter/argument_value.rb +5 -1
- data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
- data/lib/graphql/execution/interpreter/resolve.rb +23 -25
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +63 -5
- data/lib/graphql/execution/interpreter/runtime.rb +321 -222
- data/lib/graphql/execution/interpreter.rb +23 -30
- data/lib/graphql/execution/lookahead.rb +18 -11
- data/lib/graphql/execution/multiplex.rb +6 -5
- data/lib/graphql/introspection/directive_location_enum.rb +1 -1
- data/lib/graphql/introspection/directive_type.rb +1 -1
- data/lib/graphql/introspection/entry_points.rb +2 -2
- data/lib/graphql/introspection/field_type.rb +1 -1
- data/lib/graphql/introspection/schema_type.rb +6 -11
- data/lib/graphql/introspection/type_type.rb +5 -5
- data/lib/graphql/invalid_name_error.rb +1 -1
- data/lib/graphql/invalid_null_error.rb +20 -17
- data/lib/graphql/language/cache.rb +13 -0
- data/lib/graphql/language/comment.rb +18 -0
- data/lib/graphql/language/document_from_schema_definition.rb +64 -35
- data/lib/graphql/language/lexer.rb +72 -42
- data/lib/graphql/language/nodes.rb +93 -52
- data/lib/graphql/language/parser.rb +168 -61
- data/lib/graphql/language/printer.rb +31 -15
- data/lib/graphql/language/sanitized_printer.rb +1 -1
- data/lib/graphql/language.rb +61 -1
- data/lib/graphql/pagination/connection.rb +1 -1
- data/lib/graphql/query/context/scoped_context.rb +1 -1
- data/lib/graphql/query/context.rb +46 -47
- data/lib/graphql/query/null_context.rb +3 -5
- data/lib/graphql/query/partial.rb +179 -0
- data/lib/graphql/query/validation_pipeline.rb +2 -2
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/query.rb +123 -69
- data/lib/graphql/railtie.rb +7 -0
- data/lib/graphql/rubocop/graphql/base_cop.rb +1 -1
- data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
- data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
- data/lib/graphql/rubocop.rb +2 -0
- data/lib/graphql/schema/addition.rb +26 -13
- data/lib/graphql/schema/always_visible.rb +7 -2
- data/lib/graphql/schema/argument.rb +57 -8
- data/lib/graphql/schema/build_from_definition.rb +116 -49
- data/lib/graphql/schema/directive/flagged.rb +4 -2
- data/lib/graphql/schema/directive.rb +54 -2
- data/lib/graphql/schema/enum.rb +107 -24
- data/lib/graphql/schema/enum_value.rb +10 -2
- data/lib/graphql/schema/field/connection_extension.rb +1 -1
- data/lib/graphql/schema/field/scope_extension.rb +1 -1
- data/lib/graphql/schema/field.rb +134 -45
- data/lib/graphql/schema/field_extension.rb +1 -1
- data/lib/graphql/schema/has_single_input_argument.rb +6 -2
- data/lib/graphql/schema/input_object.rb +122 -64
- data/lib/graphql/schema/interface.rb +23 -5
- data/lib/graphql/schema/introspection_system.rb +6 -17
- data/lib/graphql/schema/late_bound_type.rb +4 -0
- data/lib/graphql/schema/list.rb +3 -3
- data/lib/graphql/schema/loader.rb +3 -2
- data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
- data/lib/graphql/schema/member/has_arguments.rb +44 -58
- data/lib/graphql/schema/member/has_dataloader.rb +62 -0
- data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
- data/lib/graphql/schema/member/has_directives.rb +4 -4
- data/lib/graphql/schema/member/has_fields.rb +26 -6
- data/lib/graphql/schema/member/has_interfaces.rb +6 -6
- data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
- data/lib/graphql/schema/member/has_validators.rb +1 -1
- data/lib/graphql/schema/member/relay_shortcuts.rb +1 -1
- data/lib/graphql/schema/member/type_system_helpers.rb +17 -4
- data/lib/graphql/schema/member.rb +1 -0
- data/lib/graphql/schema/mutation.rb +7 -0
- data/lib/graphql/schema/object.rb +25 -8
- data/lib/graphql/schema/printer.rb +1 -0
- data/lib/graphql/schema/ractor_shareable.rb +79 -0
- data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
- data/lib/graphql/schema/resolver.rb +29 -23
- data/lib/graphql/schema/scalar.rb +1 -6
- data/lib/graphql/schema/subscription.rb +52 -6
- data/lib/graphql/schema/timeout.rb +19 -2
- data/lib/graphql/schema/type_expression.rb +2 -2
- data/lib/graphql/schema/union.rb +1 -1
- data/lib/graphql/schema/validator/all_validator.rb +62 -0
- data/lib/graphql/schema/validator/required_validator.rb +92 -11
- data/lib/graphql/schema/validator.rb +3 -1
- data/lib/graphql/schema/visibility/migration.rb +188 -0
- data/lib/graphql/schema/visibility/profile.rb +445 -0
- data/lib/graphql/schema/visibility/visit.rb +190 -0
- data/lib/graphql/schema/visibility.rb +311 -0
- data/lib/graphql/schema/warden.rb +190 -20
- data/lib/graphql/schema.rb +695 -167
- data/lib/graphql/static_validation/all_rules.rb +2 -2
- data/lib/graphql/static_validation/base_visitor.rb +6 -5
- data/lib/graphql/static_validation/literal_validator.rb +4 -4
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
- data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +12 -2
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +88 -25
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
- data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
- data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
- data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
- data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
- data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +4 -4
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
- data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
- data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +7 -3
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
- data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
- data/lib/graphql/static_validation/validation_context.rb +18 -2
- data/lib/graphql/static_validation/validator.rb +6 -1
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +5 -3
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +11 -5
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
- data/lib/graphql/subscriptions/event.rb +13 -2
- data/lib/graphql/subscriptions/serialize.rb +1 -1
- data/lib/graphql/subscriptions.rb +7 -5
- data/lib/graphql/testing/helpers.rb +48 -16
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
- data/lib/graphql/tracing/appoptics_trace.rb +5 -1
- data/lib/graphql/tracing/appoptics_tracing.rb +7 -0
- data/lib/graphql/tracing/appsignal_trace.rb +32 -59
- data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
- data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
- data/lib/graphql/tracing/data_dog_trace.rb +46 -162
- data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
- data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
- data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
- data/lib/graphql/tracing/detailed_trace.rb +141 -0
- data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
- data/lib/graphql/tracing/legacy_trace.rb +4 -61
- data/lib/graphql/tracing/monitor_trace.rb +283 -0
- data/lib/graphql/tracing/new_relic_trace.rb +47 -54
- data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
- data/lib/graphql/tracing/notifications_trace.rb +183 -37
- data/lib/graphql/tracing/notifications_tracing.rb +2 -0
- data/lib/graphql/tracing/null_trace.rb +9 -0
- data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
- data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
- data/lib/graphql/tracing/perfetto_trace.rb +818 -0
- data/lib/graphql/tracing/platform_tracing.rb +1 -1
- data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
- data/lib/graphql/tracing/prometheus_trace.rb +73 -73
- data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
- data/lib/graphql/tracing/scout_trace.rb +32 -58
- data/lib/graphql/tracing/scout_tracing.rb +2 -0
- data/lib/graphql/tracing/sentry_trace.rb +64 -98
- data/lib/graphql/tracing/statsd_trace.rb +33 -45
- data/lib/graphql/tracing/statsd_tracing.rb +2 -0
- data/lib/graphql/tracing/trace.rb +111 -1
- data/lib/graphql/tracing.rb +31 -30
- data/lib/graphql/type_kinds.rb +2 -1
- data/lib/graphql/types/relay/connection_behaviors.rb +12 -2
- data/lib/graphql/types/relay/edge_behaviors.rb +11 -1
- data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
- data/lib/graphql/types.rb +18 -11
- data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +64 -54
- metadata +197 -22
- data/lib/graphql/analysis/ast/analyzer.rb +0 -91
- data/lib/graphql/analysis/ast/field_usage.rb +0 -82
- data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
- data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
- data/lib/graphql/analysis/ast/query_complexity.rb +0 -182
- data/lib/graphql/analysis/ast/visitor.rb +0 -276
- data/lib/graphql/analysis/ast.rb +0 -94
- data/lib/graphql/backtrace/inspect_result.rb +0 -50
- data/lib/graphql/backtrace/trace.rb +0 -93
- data/lib/graphql/backtrace/tracer.rb +0 -80
- data/lib/graphql/language/token.rb +0 -34
- data/lib/graphql/schema/invalid_type_error.rb +0 -7
- data/lib/graphql/schema/null_mask.rb +0 -11
- data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92559d0138e0b495e6bc3e571fb804c6ace53a9b60facf3711323bafe3f7970a
|
|
4
|
+
data.tar.gz: 1acd789d92c34070bbe768d4f390f58a540e99627e9d3171b25b423d2a0356f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c72b2c1092240a9c5e25e331fed78a24da102e1cb0afa998c503010ced4359333ea24f735c705dfed6d3fb33307215d15e12202064ad4ee1b9bd8a1f7b57acc
|
|
7
|
+
data.tar.gz: 2606dd0c0a569c263c357873bcf7fe53bd19f990ac8b20cbaab0ef06daedb52ffdbac4cc483d6256066513895a64056312c56a51d7f483a7882b5cf57c99a0e9
|
|
@@ -9,7 +9,7 @@ module Graphql
|
|
|
9
9
|
class MutationRootGenerator < Rails::Generators::Base
|
|
10
10
|
include Core
|
|
11
11
|
|
|
12
|
-
desc "Create mutation base type, mutation root
|
|
12
|
+
desc "Create mutation base type, mutation root type, and adds the latter to the schema"
|
|
13
13
|
source_root File.expand_path('../templates', __FILE__)
|
|
14
14
|
|
|
15
15
|
class_option :schema,
|
|
@@ -31,4 +31,4 @@ module Graphql
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
|
-
end
|
|
34
|
+
end
|
|
@@ -45,6 +45,13 @@ module Graphql
|
|
|
45
45
|
# post "/graphql", to: "graphql#execute"
|
|
46
46
|
# ```
|
|
47
47
|
#
|
|
48
|
+
# Add ActiveRecord::QueryLogs metadata:
|
|
49
|
+
# ```ruby
|
|
50
|
+
# current_graphql_operation: -> { GraphQL::Current.operation_name },
|
|
51
|
+
# current_graphql_field: -> { GraphQL::Current.field&.path },
|
|
52
|
+
# current_dataloader_source: -> { GraphQL::Current.dataloader_source_class },
|
|
53
|
+
# ```
|
|
54
|
+
#
|
|
48
55
|
# Accept a `--batch` option which adds `GraphQL::Batch` setup.
|
|
49
56
|
#
|
|
50
57
|
# Use `--skip-graphiql` to skip `graphiql-rails` installation.
|
|
@@ -92,6 +99,11 @@ module Graphql
|
|
|
92
99
|
default: false,
|
|
93
100
|
desc: "Use GraphQL Playground over Graphiql as IDE"
|
|
94
101
|
|
|
102
|
+
class_option :skip_query_logs,
|
|
103
|
+
type: :boolean,
|
|
104
|
+
default: false,
|
|
105
|
+
desc: "Skip ActiveRecord::QueryLogs hooks in config/application.rb"
|
|
106
|
+
|
|
95
107
|
# These two options are taken from Rails' own generators'
|
|
96
108
|
class_option :api,
|
|
97
109
|
type: :boolean,
|
|
@@ -180,6 +192,40 @@ RUBY
|
|
|
180
192
|
install_relay
|
|
181
193
|
end
|
|
182
194
|
|
|
195
|
+
if !options[:skip_query_logs]
|
|
196
|
+
config_file = "config/application.rb"
|
|
197
|
+
current_app_rb = File.read(Rails.root.join(config_file))
|
|
198
|
+
existing_log_tags_pattern = /config.active_record.query_log_tags = \[\n?(\s*:[a-z_]+,?\s*\n?|\s*#[^\]]*\n)*/m
|
|
199
|
+
existing_log_tags = existing_log_tags_pattern.match(current_app_rb)
|
|
200
|
+
if existing_log_tags && behavior == :invoke
|
|
201
|
+
code = <<-RUBY
|
|
202
|
+
# GraphQL-Ruby query log tags:
|
|
203
|
+
current_graphql_operation: -> { GraphQL::Current.operation_name },
|
|
204
|
+
current_graphql_field: -> { GraphQL::Current.field&.path },
|
|
205
|
+
current_dataloader_source: -> { GraphQL::Current.dataloader_source_class },
|
|
206
|
+
RUBY
|
|
207
|
+
if !existing_log_tags.to_s.end_with?(",")
|
|
208
|
+
code = ",\n#{code} "
|
|
209
|
+
end
|
|
210
|
+
# Try to insert this code _after_ any plain symbol entries in the array of query log tags:
|
|
211
|
+
after_code = existing_log_tags_pattern
|
|
212
|
+
else
|
|
213
|
+
code = <<-RUBY
|
|
214
|
+
config.active_record.query_log_tags_enabled = true
|
|
215
|
+
config.active_record.query_log_tags = [
|
|
216
|
+
# Rails query log tags:
|
|
217
|
+
:application, :controller, :action, :job,
|
|
218
|
+
# GraphQL-Ruby query log tags:
|
|
219
|
+
current_graphql_operation: -> { GraphQL::Current.operation_name },
|
|
220
|
+
current_graphql_field: -> { GraphQL::Current.field&.path },
|
|
221
|
+
current_dataloader_source: -> { GraphQL::Current.dataloader_source_class },
|
|
222
|
+
]
|
|
223
|
+
RUBY
|
|
224
|
+
after_code = "class Application < Rails::Application\n"
|
|
225
|
+
end
|
|
226
|
+
insert_into_file(config_file, code, after: after_code)
|
|
227
|
+
end
|
|
228
|
+
|
|
183
229
|
if gemfile_modified?
|
|
184
230
|
say "Gemfile has been modified, make sure you `bundle install`"
|
|
185
231
|
end
|
|
@@ -18,7 +18,7 @@ module Graphql
|
|
|
18
18
|
class_option :orm, banner: "NAME", type: :string, required: true,
|
|
19
19
|
desc: "ORM to generate the controller for"
|
|
20
20
|
|
|
21
|
-
class_option
|
|
21
|
+
class_option :namespaced_types,
|
|
22
22
|
type: :boolean,
|
|
23
23
|
required: false,
|
|
24
24
|
default: false,
|
|
@@ -26,6 +26,9 @@ class <%= schema_name %> < GraphQL::Schema
|
|
|
26
26
|
raise(GraphQL::RequiredImplementationMissingError)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Limit the size of incoming queries:
|
|
30
|
+
max_query_string_tokens(5000)
|
|
31
|
+
|
|
29
32
|
# Stop validating when it encounters this many errors:
|
|
30
33
|
validate_max_errors(100)
|
|
31
34
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Analysis
|
|
4
|
+
# Query analyzer for query ASTs. Query analyzers respond to visitor style methods
|
|
5
|
+
# but are prefixed by `enter` and `leave`.
|
|
6
|
+
#
|
|
7
|
+
# When an analyzer is initialized with a Multiplex, you can always get the current query from
|
|
8
|
+
# `visitor.query` in the visit methods.
|
|
9
|
+
#
|
|
10
|
+
# @param [GraphQL::Query, GraphQL::Execution::Multiplex] The query or multiplex to analyze
|
|
11
|
+
class Analyzer
|
|
12
|
+
def initialize(subject)
|
|
13
|
+
@subject = subject
|
|
14
|
+
|
|
15
|
+
if subject.is_a?(GraphQL::Query)
|
|
16
|
+
@query = subject
|
|
17
|
+
@multiplex = nil
|
|
18
|
+
else
|
|
19
|
+
@multiplex = subject
|
|
20
|
+
@query = nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Analyzer hook to decide at analysis time whether a query should
|
|
25
|
+
# be analyzed or not.
|
|
26
|
+
# @return [Boolean] If the query should be analyzed or not
|
|
27
|
+
def analyze?
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Analyzer hook to decide at analysis time whether analysis
|
|
32
|
+
# requires a visitor pass; can be disabled for precomputed results.
|
|
33
|
+
# @return [Boolean] If analysis requires visitation or not
|
|
34
|
+
def visit?
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The result for this analyzer. Returning {GraphQL::AnalysisError} results
|
|
39
|
+
# in a query error.
|
|
40
|
+
# @return [Any] The analyzer result
|
|
41
|
+
def result
|
|
42
|
+
raise GraphQL::RequiredImplementationMissingError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
|
|
46
|
+
class << self
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_visitor_hooks(member_name)
|
|
50
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
|
51
|
+
def on_enter_#{member_name}(node, parent, visitor)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_leave_#{member_name}(node, parent, visitor)
|
|
55
|
+
end
|
|
56
|
+
EOS
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
build_visitor_hooks :argument
|
|
61
|
+
build_visitor_hooks :directive
|
|
62
|
+
build_visitor_hooks :document
|
|
63
|
+
build_visitor_hooks :enum
|
|
64
|
+
build_visitor_hooks :field
|
|
65
|
+
build_visitor_hooks :fragment_spread
|
|
66
|
+
build_visitor_hooks :inline_fragment
|
|
67
|
+
build_visitor_hooks :input_object
|
|
68
|
+
build_visitor_hooks :list_type
|
|
69
|
+
build_visitor_hooks :non_null_type
|
|
70
|
+
build_visitor_hooks :null_value
|
|
71
|
+
build_visitor_hooks :operation_definition
|
|
72
|
+
build_visitor_hooks :type_name
|
|
73
|
+
build_visitor_hooks :variable_definition
|
|
74
|
+
build_visitor_hooks :variable_identifier
|
|
75
|
+
build_visitor_hooks :abstract_node
|
|
76
|
+
# rubocop:enable Development/NoEvalCop
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
# @return [GraphQL::Query, GraphQL::Execution::Multiplex] Whatever this analyzer is analyzing
|
|
80
|
+
attr_reader :subject
|
|
81
|
+
|
|
82
|
+
# @return [GraphQL::Query, nil] `nil` if this analyzer is visiting a multiplex
|
|
83
|
+
# (When this is `nil`, use `visitor.query` inside visit methods to get the current query)
|
|
84
|
+
attr_reader :query
|
|
85
|
+
|
|
86
|
+
# @return [GraphQL::Execution::Multiplex, nil] `nil` if this analyzer is visiting a query
|
|
87
|
+
attr_reader :multiplex
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Analysis
|
|
4
|
+
class FieldUsage < Analyzer
|
|
5
|
+
def initialize(query)
|
|
6
|
+
super
|
|
7
|
+
@used_fields = Set.new
|
|
8
|
+
@used_deprecated_fields = Set.new
|
|
9
|
+
@used_deprecated_arguments = Set.new
|
|
10
|
+
@used_deprecated_enum_values = Set.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def on_leave_field(node, parent, visitor)
|
|
14
|
+
field_defn = visitor.field_definition
|
|
15
|
+
field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
|
|
16
|
+
@used_fields << field
|
|
17
|
+
@used_deprecated_fields << field if field_defn.deprecation_reason
|
|
18
|
+
arguments = visitor.query.arguments_for(node, field_defn)
|
|
19
|
+
# If there was an error when preparing this argument object,
|
|
20
|
+
# then this might be an error or something:
|
|
21
|
+
if arguments.respond_to?(:argument_values)
|
|
22
|
+
extract_deprecated_arguments(arguments.argument_values)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def result
|
|
27
|
+
{
|
|
28
|
+
used_fields: @used_fields.to_a,
|
|
29
|
+
used_deprecated_fields: @used_deprecated_fields.to_a,
|
|
30
|
+
used_deprecated_arguments: @used_deprecated_arguments.to_a,
|
|
31
|
+
used_deprecated_enum_values: @used_deprecated_enum_values.to_a,
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def extract_deprecated_arguments(argument_values)
|
|
38
|
+
argument_values.each_pair do |_argument_name, argument|
|
|
39
|
+
if argument.definition.deprecation_reason
|
|
40
|
+
@used_deprecated_arguments << argument.definition.path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
arg_val = argument.value
|
|
44
|
+
|
|
45
|
+
next if arg_val.nil?
|
|
46
|
+
|
|
47
|
+
argument_type = argument.definition.type
|
|
48
|
+
if argument_type.non_null?
|
|
49
|
+
argument_type = argument_type.of_type
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if argument_type.kind.input_object?
|
|
53
|
+
extract_deprecated_arguments(argument.original_value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
|
|
54
|
+
elsif argument_type.kind.enum?
|
|
55
|
+
extract_deprecated_enum_value(argument_type, arg_val)
|
|
56
|
+
elsif argument_type.list?
|
|
57
|
+
inner_type = argument_type.unwrap
|
|
58
|
+
case inner_type.kind
|
|
59
|
+
when TypeKinds::INPUT_OBJECT
|
|
60
|
+
argument.original_value.each do |value|
|
|
61
|
+
extract_deprecated_arguments(value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
|
|
62
|
+
end
|
|
63
|
+
when TypeKinds::ENUM
|
|
64
|
+
arg_val.each do |value|
|
|
65
|
+
extract_deprecated_enum_value(inner_type, value)
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
# Not a kind of input that we track
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_deprecated_enum_value(enum_type, value)
|
|
75
|
+
enum_value = @query.types.enum_values(enum_type).find { |ev| ev.value == value }
|
|
76
|
+
if enum_value&.deprecation_reason
|
|
77
|
+
@used_deprecated_enum_values << enum_value.path
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Analysis
|
|
4
|
+
# Used under the hood to implement complexity validation,
|
|
5
|
+
# see {Schema#max_complexity} and {Query#max_complexity}
|
|
6
|
+
class MaxQueryComplexity < QueryComplexity
|
|
7
|
+
def result
|
|
8
|
+
return if subject.max_complexity.nil?
|
|
9
|
+
|
|
10
|
+
total_complexity = max_possible_complexity
|
|
11
|
+
|
|
12
|
+
if total_complexity > subject.max_complexity
|
|
13
|
+
GraphQL::AnalysisError.new("Query has complexity of #{total_complexity}, which exceeds max complexity of #{subject.max_complexity}")
|
|
14
|
+
else
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Analysis
|
|
4
|
+
class MaxQueryDepth < QueryDepth
|
|
5
|
+
def result
|
|
6
|
+
configured_max_depth = if query
|
|
7
|
+
query.max_depth
|
|
8
|
+
else
|
|
9
|
+
multiplex.schema.max_depth
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
if configured_max_depth && @max_depth > configured_max_depth
|
|
13
|
+
GraphQL::AnalysisError.new("Query has depth of #{@max_depth}, which exceeds max depth of #{configured_max_depth}")
|
|
14
|
+
else
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
module Analysis
|
|
4
|
+
# Calculate the complexity of a query, using {Field#complexity} values.
|
|
5
|
+
class QueryComplexity < Analyzer
|
|
6
|
+
# State for the query complexity calculation:
|
|
7
|
+
# - `complexities_on_type` holds complexity scores for each type
|
|
8
|
+
def initialize(query)
|
|
9
|
+
super
|
|
10
|
+
@skip_introspection_fields = !query.schema.max_complexity_count_introspection_fields
|
|
11
|
+
@complexities_on_type_by_query = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Override this method to use the complexity result
|
|
15
|
+
def result
|
|
16
|
+
case subject.schema.complexity_cost_calculation_mode_for(subject.context)
|
|
17
|
+
when :future
|
|
18
|
+
max_possible_complexity
|
|
19
|
+
when :legacy
|
|
20
|
+
max_possible_complexity(mode: :legacy)
|
|
21
|
+
when :compare
|
|
22
|
+
future_complexity = max_possible_complexity
|
|
23
|
+
legacy_complexity = max_possible_complexity(mode: :legacy)
|
|
24
|
+
if future_complexity != legacy_complexity
|
|
25
|
+
subject.schema.legacy_complexity_cost_calculation_mismatch(subject, future_complexity, legacy_complexity)
|
|
26
|
+
else
|
|
27
|
+
future_complexity
|
|
28
|
+
end
|
|
29
|
+
when nil
|
|
30
|
+
subject.logger.warn <<~GRAPHQL
|
|
31
|
+
GraphQL-Ruby's complexity cost system is getting some "breaking fixes" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method
|
|
32
|
+
|
|
33
|
+
To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with:
|
|
34
|
+
|
|
35
|
+
complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`
|
|
36
|
+
|
|
37
|
+
GRAPHQL
|
|
38
|
+
max_possible_complexity(mode: :legacy)
|
|
39
|
+
else
|
|
40
|
+
raise ArgumentError, "Expected `:future`, `:legacy`, `:compare`, or `nil` from `#{query.schema}.complexity_cost_calculation_mode_for` but got: #{query.schema.complexity_cost_calculation_mode.inspect}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# ScopedTypeComplexity models a tree of GraphQL types mapped to inner selections, ie:
|
|
45
|
+
# Hash<GraphQL::BaseType, Hash<String, ScopedTypeComplexity>>
|
|
46
|
+
class ScopedTypeComplexity < Hash
|
|
47
|
+
# A proc for defaulting empty namespace requests as a new scope hash.
|
|
48
|
+
DEFAULT_PROC = ->(h, k) { h[k] = {} }
|
|
49
|
+
|
|
50
|
+
attr_reader :field_definition, :response_path, :query
|
|
51
|
+
|
|
52
|
+
# @param parent_type [Class] The owner of `field_definition`
|
|
53
|
+
# @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration
|
|
54
|
+
# @param query [GraphQL::Query] Used for `query.possible_types`
|
|
55
|
+
# @param response_path [Array<String>] The path to the response key for the field
|
|
56
|
+
# @return [Hash<GraphQL::BaseType, Hash<String, ScopedTypeComplexity>>]
|
|
57
|
+
def initialize(parent_type, field_definition, query, response_path)
|
|
58
|
+
super(&DEFAULT_PROC)
|
|
59
|
+
@parent_type = parent_type
|
|
60
|
+
@field_definition = field_definition
|
|
61
|
+
@query = query
|
|
62
|
+
@response_path = response_path
|
|
63
|
+
@nodes = []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Array<GraphQL::Language::Nodes::Field>]
|
|
67
|
+
attr_reader :nodes
|
|
68
|
+
|
|
69
|
+
def own_complexity(child_complexity)
|
|
70
|
+
@field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def composite?
|
|
74
|
+
!empty?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_enter_field(node, parent, visitor)
|
|
79
|
+
# We don't want to visit fragment definitions,
|
|
80
|
+
# we'll visit them when we hit the spreads instead
|
|
81
|
+
return if visitor.visiting_fragment_definition?
|
|
82
|
+
return if visitor.skipping?
|
|
83
|
+
return if @skip_introspection_fields && visitor.field_definition.introspection?
|
|
84
|
+
parent_type = visitor.parent_type_definition
|
|
85
|
+
field_key = node.alias || node.name
|
|
86
|
+
|
|
87
|
+
# Find or create a complexity scope stack for this query.
|
|
88
|
+
scopes_stack = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]
|
|
89
|
+
|
|
90
|
+
# Find or create the complexity costing node for this field.
|
|
91
|
+
scope = scopes_stack.last[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
|
|
92
|
+
scope.nodes.push(node)
|
|
93
|
+
scopes_stack.push(scope)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def on_leave_field(node, parent, visitor)
|
|
97
|
+
# We don't want to visit fragment definitions,
|
|
98
|
+
# we'll visit them when we hit the spreads instead
|
|
99
|
+
return if visitor.visiting_fragment_definition?
|
|
100
|
+
return if visitor.skipping?
|
|
101
|
+
return if @skip_introspection_fields && visitor.field_definition.introspection?
|
|
102
|
+
scopes_stack = @complexities_on_type_by_query[visitor.query]
|
|
103
|
+
scopes_stack.pop
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# @return [Integer]
|
|
109
|
+
def max_possible_complexity(mode: :future)
|
|
110
|
+
@complexities_on_type_by_query.reduce(0) do |total, (query, scopes_stack)|
|
|
111
|
+
total + merged_max_complexity_for_scopes(query, [scopes_stack.first], mode)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param query [GraphQL::Query] Used for `query.possible_types`
|
|
116
|
+
# @param scopes [Array<ScopedTypeComplexity>] Array of scoped type complexities
|
|
117
|
+
# @param mode [:future, :legacy]
|
|
118
|
+
# @return [Integer]
|
|
119
|
+
def merged_max_complexity_for_scopes(query, scopes, mode)
|
|
120
|
+
# Aggregate a set of all possible scope types encountered (scope keys).
|
|
121
|
+
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
|
122
|
+
possible_scope_types = scopes.each_with_object({}) do |scope, memo|
|
|
123
|
+
memo.merge!(scope)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Expand abstract scope types into their concrete implementations;
|
|
127
|
+
# overlapping abstracts coalesce through their intersecting types.
|
|
128
|
+
possible_scope_types.keys.each do |possible_scope_type|
|
|
129
|
+
next unless possible_scope_type.kind.abstract?
|
|
130
|
+
|
|
131
|
+
query.types.possible_types(possible_scope_type).each do |impl_type|
|
|
132
|
+
possible_scope_types[impl_type] ||= true
|
|
133
|
+
end
|
|
134
|
+
possible_scope_types.delete(possible_scope_type)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Aggregate the lexical selections that may apply to each possible type,
|
|
138
|
+
# and then return the maximum cost among possible typed selections.
|
|
139
|
+
possible_scope_types.each_key.reduce(0) do |max, possible_scope_type|
|
|
140
|
+
# Collect inner selections from all scopes that intersect with this possible type.
|
|
141
|
+
all_inner_selections = scopes.each_with_object([]) do |scope, memo|
|
|
142
|
+
scope.each do |scope_type, inner_selections|
|
|
143
|
+
memo << inner_selections if types_intersect?(query, scope_type, possible_scope_type)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Find the maximum complexity for the scope type among possible lexical branches.
|
|
148
|
+
complexity = case mode
|
|
149
|
+
when :legacy
|
|
150
|
+
legacy_merged_max_complexity(query, all_inner_selections)
|
|
151
|
+
when :future
|
|
152
|
+
merged_max_complexity(query, all_inner_selections)
|
|
153
|
+
else
|
|
154
|
+
raise ArgumentError, "Expected :legacy or :future, not: #{mode.inspect}"
|
|
155
|
+
end
|
|
156
|
+
complexity > max ? complexity : max
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def types_intersect?(query, a, b)
|
|
161
|
+
return true if a == b
|
|
162
|
+
a_types = query.types.possible_types(a)
|
|
163
|
+
query.types.possible_types(b).any? { |t| a_types.include?(t) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# A hook which is called whenever a field's max complexity is calculated.
|
|
167
|
+
# Override this method to capture individual field complexity details.
|
|
168
|
+
#
|
|
169
|
+
# @param scoped_type_complexity [ScopedTypeComplexity]
|
|
170
|
+
# @param max_complexity [Numeric] Field's maximum complexity including child complexity
|
|
171
|
+
# @param child_complexity [Numeric, nil] Field's child complexity
|
|
172
|
+
def field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @param inner_selections [Array<Hash<String, ScopedTypeComplexity>>] Field selections for a scope
|
|
176
|
+
# @return [Integer] Total complexity value for all these selections in the parent scope
|
|
177
|
+
def merged_max_complexity(query, inner_selections)
|
|
178
|
+
# Aggregate a set of all unique field selection keys across all scopes.
|
|
179
|
+
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
|
180
|
+
unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo|
|
|
181
|
+
memo.merge!(inner_selection)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Add up the total cost for each unique field name's coalesced selections
|
|
185
|
+
unique_field_keys.each_key.reduce(0) do |total, field_key|
|
|
186
|
+
# Collect all child scopes for this field key;
|
|
187
|
+
# all keys come with at least one scope.
|
|
188
|
+
child_scopes = inner_selections.filter_map { _1[field_key] }
|
|
189
|
+
|
|
190
|
+
# Compute maximum possible cost of child selections;
|
|
191
|
+
# composites merge their maximums, while leaf scopes are always zero.
|
|
192
|
+
# FieldsWillMerge validation assures all scopes are uniformly composite or leaf.
|
|
193
|
+
maximum_children_cost = if child_scopes.any?(&:composite?)
|
|
194
|
+
merged_max_complexity_for_scopes(query, child_scopes, :future)
|
|
195
|
+
else
|
|
196
|
+
0
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Identify the maximum cost and scope among possibilities
|
|
200
|
+
maximum_cost = 0
|
|
201
|
+
maximum_scope = child_scopes.reduce(child_scopes.last) do |max_scope, possible_scope|
|
|
202
|
+
scope_cost = possible_scope.own_complexity(maximum_children_cost)
|
|
203
|
+
if scope_cost > maximum_cost
|
|
204
|
+
maximum_cost = scope_cost
|
|
205
|
+
possible_scope
|
|
206
|
+
else
|
|
207
|
+
max_scope
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
field_complexity(
|
|
212
|
+
maximum_scope,
|
|
213
|
+
max_complexity: maximum_cost,
|
|
214
|
+
child_complexity: maximum_children_cost,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
total + maximum_cost
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def legacy_merged_max_complexity(query, inner_selections)
|
|
222
|
+
# Aggregate a set of all unique field selection keys across all scopes.
|
|
223
|
+
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
|
224
|
+
unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo|
|
|
225
|
+
memo.merge!(inner_selection)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Add up the total cost for each unique field name's coalesced selections
|
|
229
|
+
unique_field_keys.each_key.reduce(0) do |total, field_key|
|
|
230
|
+
composite_scopes = nil
|
|
231
|
+
field_cost = 0
|
|
232
|
+
|
|
233
|
+
# Collect composite selection scopes for further aggregation,
|
|
234
|
+
# leaf selections report their costs directly.
|
|
235
|
+
inner_selections.each do |inner_selection|
|
|
236
|
+
child_scope = inner_selection[field_key]
|
|
237
|
+
next unless child_scope
|
|
238
|
+
|
|
239
|
+
# Empty child scopes are leaf nodes with zero child complexity.
|
|
240
|
+
if child_scope.empty?
|
|
241
|
+
field_cost = child_scope.own_complexity(0)
|
|
242
|
+
field_complexity(child_scope, max_complexity: field_cost, child_complexity: nil)
|
|
243
|
+
else
|
|
244
|
+
composite_scopes ||= []
|
|
245
|
+
composite_scopes << child_scope
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
if composite_scopes
|
|
250
|
+
child_complexity = merged_max_complexity_for_scopes(query, composite_scopes, :legacy)
|
|
251
|
+
|
|
252
|
+
# This is the last composite scope visited; assume it's representative (for backwards compatibility).
|
|
253
|
+
# Note: it would be more correct to score each composite scope and use the maximum possibility.
|
|
254
|
+
field_cost = composite_scopes.last.own_complexity(child_complexity)
|
|
255
|
+
field_complexity(composite_scopes.last, max_complexity: field_cost, child_complexity: child_complexity)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
total + field_cost
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -23,37 +23,35 @@ module GraphQL
|
|
|
23
23
|
# Schema.execute(query_str)
|
|
24
24
|
# # GraphQL query depth: 8
|
|
25
25
|
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
26
|
+
class QueryDepth < Analyzer
|
|
27
|
+
def initialize(query)
|
|
28
|
+
@max_depth = 0
|
|
29
|
+
@current_depth = 0
|
|
30
|
+
@count_introspection_fields = query.schema.count_introspection_fields
|
|
31
|
+
super
|
|
32
|
+
end
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
def on_enter_field(node, parent, visitor)
|
|
35
|
+
return if visitor.skipping? ||
|
|
36
|
+
visitor.visiting_fragment_definition? ||
|
|
37
|
+
(@count_introspection_fields == false && visitor.field_definition.introspection?)
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
@current_depth += 1
|
|
40
|
+
end
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
def on_leave_field(node, parent, visitor)
|
|
43
|
+
return if visitor.skipping? ||
|
|
44
|
+
visitor.visiting_fragment_definition? ||
|
|
45
|
+
(@count_introspection_fields == false && visitor.field_definition.introspection?)
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
51
|
-
@current_depth -= 1
|
|
47
|
+
if @max_depth < @current_depth
|
|
48
|
+
@max_depth = @current_depth
|
|
52
49
|
end
|
|
50
|
+
@current_depth -= 1
|
|
51
|
+
end
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
end
|
|
53
|
+
def result
|
|
54
|
+
@max_depth
|
|
57
55
|
end
|
|
58
56
|
end
|
|
59
57
|
end
|