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.
Files changed (240) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install/mutation_root_generator.rb +2 -2
  3. data/lib/generators/graphql/install_generator.rb +46 -0
  4. data/lib/generators/graphql/orm_mutations_base.rb +1 -1
  5. data/lib/generators/graphql/templates/base_resolver.erb +2 -0
  6. data/lib/generators/graphql/templates/schema.erb +3 -0
  7. data/lib/generators/graphql/type_generator.rb +1 -1
  8. data/lib/graphql/analysis/analyzer.rb +90 -0
  9. data/lib/graphql/analysis/field_usage.rb +82 -0
  10. data/lib/graphql/analysis/max_query_complexity.rb +20 -0
  11. data/lib/graphql/analysis/max_query_depth.rb +20 -0
  12. data/lib/graphql/analysis/query_complexity.rb +263 -0
  13. data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
  14. data/lib/graphql/analysis/visitor.rb +280 -0
  15. data/lib/graphql/analysis.rb +95 -1
  16. data/lib/graphql/autoload.rb +38 -0
  17. data/lib/graphql/backtrace/table.rb +118 -55
  18. data/lib/graphql/backtrace.rb +1 -19
  19. data/lib/graphql/current.rb +57 -0
  20. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  21. data/lib/graphql/dashboard/installable.rb +22 -0
  22. data/lib/graphql/dashboard/limiters.rb +93 -0
  23. data/lib/graphql/dashboard/operation_store.rb +199 -0
  24. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  25. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  26. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  27. data/lib/graphql/dashboard/statics/dashboard.css +30 -0
  28. data/lib/graphql/dashboard/statics/dashboard.js +143 -0
  29. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  30. data/lib/graphql/dashboard/statics/icon.png +0 -0
  31. data/lib/graphql/dashboard/subscriptions.rb +96 -0
  32. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  33. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  34. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  35. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  36. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
  37. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  38. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  39. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  40. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  41. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  42. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  43. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  44. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  45. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  46. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  47. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +108 -0
  48. data/lib/graphql/dashboard.rb +158 -0
  49. data/lib/graphql/dataloader/active_record_association_source.rb +84 -0
  50. data/lib/graphql/dataloader/active_record_source.rb +47 -0
  51. data/lib/graphql/dataloader/async_dataloader.rb +46 -19
  52. data/lib/graphql/dataloader/null_dataloader.rb +51 -10
  53. data/lib/graphql/dataloader/source.rb +20 -9
  54. data/lib/graphql/dataloader.rb +153 -45
  55. data/lib/graphql/date_encoding_error.rb +1 -1
  56. data/lib/graphql/dig.rb +2 -1
  57. data/lib/graphql/execution/interpreter/argument_value.rb +5 -1
  58. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  59. data/lib/graphql/execution/interpreter/resolve.rb +23 -25
  60. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +63 -5
  61. data/lib/graphql/execution/interpreter/runtime.rb +321 -222
  62. data/lib/graphql/execution/interpreter.rb +23 -30
  63. data/lib/graphql/execution/lookahead.rb +18 -11
  64. data/lib/graphql/execution/multiplex.rb +6 -5
  65. data/lib/graphql/introspection/directive_location_enum.rb +1 -1
  66. data/lib/graphql/introspection/directive_type.rb +1 -1
  67. data/lib/graphql/introspection/entry_points.rb +2 -2
  68. data/lib/graphql/introspection/field_type.rb +1 -1
  69. data/lib/graphql/introspection/schema_type.rb +6 -11
  70. data/lib/graphql/introspection/type_type.rb +5 -5
  71. data/lib/graphql/invalid_name_error.rb +1 -1
  72. data/lib/graphql/invalid_null_error.rb +20 -17
  73. data/lib/graphql/language/cache.rb +13 -0
  74. data/lib/graphql/language/comment.rb +18 -0
  75. data/lib/graphql/language/document_from_schema_definition.rb +64 -35
  76. data/lib/graphql/language/lexer.rb +72 -42
  77. data/lib/graphql/language/nodes.rb +93 -52
  78. data/lib/graphql/language/parser.rb +168 -61
  79. data/lib/graphql/language/printer.rb +31 -15
  80. data/lib/graphql/language/sanitized_printer.rb +1 -1
  81. data/lib/graphql/language.rb +61 -1
  82. data/lib/graphql/pagination/connection.rb +1 -1
  83. data/lib/graphql/query/context/scoped_context.rb +1 -1
  84. data/lib/graphql/query/context.rb +46 -47
  85. data/lib/graphql/query/null_context.rb +3 -5
  86. data/lib/graphql/query/partial.rb +179 -0
  87. data/lib/graphql/query/validation_pipeline.rb +2 -2
  88. data/lib/graphql/query/variable_validation_error.rb +1 -1
  89. data/lib/graphql/query.rb +123 -69
  90. data/lib/graphql/railtie.rb +7 -0
  91. data/lib/graphql/rubocop/graphql/base_cop.rb +1 -1
  92. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  93. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  94. data/lib/graphql/rubocop.rb +2 -0
  95. data/lib/graphql/schema/addition.rb +26 -13
  96. data/lib/graphql/schema/always_visible.rb +7 -2
  97. data/lib/graphql/schema/argument.rb +57 -8
  98. data/lib/graphql/schema/build_from_definition.rb +116 -49
  99. data/lib/graphql/schema/directive/flagged.rb +4 -2
  100. data/lib/graphql/schema/directive.rb +54 -2
  101. data/lib/graphql/schema/enum.rb +107 -24
  102. data/lib/graphql/schema/enum_value.rb +10 -2
  103. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  104. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  105. data/lib/graphql/schema/field.rb +134 -45
  106. data/lib/graphql/schema/field_extension.rb +1 -1
  107. data/lib/graphql/schema/has_single_input_argument.rb +6 -2
  108. data/lib/graphql/schema/input_object.rb +122 -64
  109. data/lib/graphql/schema/interface.rb +23 -5
  110. data/lib/graphql/schema/introspection_system.rb +6 -17
  111. data/lib/graphql/schema/late_bound_type.rb +4 -0
  112. data/lib/graphql/schema/list.rb +3 -3
  113. data/lib/graphql/schema/loader.rb +3 -2
  114. data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
  115. data/lib/graphql/schema/member/has_arguments.rb +44 -58
  116. data/lib/graphql/schema/member/has_dataloader.rb +62 -0
  117. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  118. data/lib/graphql/schema/member/has_directives.rb +4 -4
  119. data/lib/graphql/schema/member/has_fields.rb +26 -6
  120. data/lib/graphql/schema/member/has_interfaces.rb +6 -6
  121. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  122. data/lib/graphql/schema/member/has_validators.rb +1 -1
  123. data/lib/graphql/schema/member/relay_shortcuts.rb +1 -1
  124. data/lib/graphql/schema/member/type_system_helpers.rb +17 -4
  125. data/lib/graphql/schema/member.rb +1 -0
  126. data/lib/graphql/schema/mutation.rb +7 -0
  127. data/lib/graphql/schema/object.rb +25 -8
  128. data/lib/graphql/schema/printer.rb +1 -0
  129. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  130. data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
  131. data/lib/graphql/schema/resolver.rb +29 -23
  132. data/lib/graphql/schema/scalar.rb +1 -6
  133. data/lib/graphql/schema/subscription.rb +52 -6
  134. data/lib/graphql/schema/timeout.rb +19 -2
  135. data/lib/graphql/schema/type_expression.rb +2 -2
  136. data/lib/graphql/schema/union.rb +1 -1
  137. data/lib/graphql/schema/validator/all_validator.rb +62 -0
  138. data/lib/graphql/schema/validator/required_validator.rb +92 -11
  139. data/lib/graphql/schema/validator.rb +3 -1
  140. data/lib/graphql/schema/visibility/migration.rb +188 -0
  141. data/lib/graphql/schema/visibility/profile.rb +445 -0
  142. data/lib/graphql/schema/visibility/visit.rb +190 -0
  143. data/lib/graphql/schema/visibility.rb +311 -0
  144. data/lib/graphql/schema/warden.rb +190 -20
  145. data/lib/graphql/schema.rb +695 -167
  146. data/lib/graphql/static_validation/all_rules.rb +2 -2
  147. data/lib/graphql/static_validation/base_visitor.rb +6 -5
  148. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  149. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  150. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
  151. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
  152. data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
  153. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  154. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +12 -2
  155. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
  156. data/lib/graphql/static_validation/rules/fields_will_merge.rb +88 -25
  157. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
  158. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  159. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
  160. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  161. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  162. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
  163. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  164. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  165. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +4 -4
  166. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
  167. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  168. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +7 -3
  169. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  170. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
  171. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  172. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
  173. data/lib/graphql/static_validation/validation_context.rb +18 -2
  174. data/lib/graphql/static_validation/validator.rb +6 -1
  175. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +5 -3
  176. data/lib/graphql/subscriptions/broadcast_analyzer.rb +11 -5
  177. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
  178. data/lib/graphql/subscriptions/event.rb +13 -2
  179. data/lib/graphql/subscriptions/serialize.rb +1 -1
  180. data/lib/graphql/subscriptions.rb +7 -5
  181. data/lib/graphql/testing/helpers.rb +48 -16
  182. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  183. data/lib/graphql/testing.rb +1 -0
  184. data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
  185. data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
  186. data/lib/graphql/tracing/appoptics_trace.rb +5 -1
  187. data/lib/graphql/tracing/appoptics_tracing.rb +7 -0
  188. data/lib/graphql/tracing/appsignal_trace.rb +32 -59
  189. data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
  190. data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
  191. data/lib/graphql/tracing/data_dog_trace.rb +46 -162
  192. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  193. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  194. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  195. data/lib/graphql/tracing/detailed_trace.rb +141 -0
  196. data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
  197. data/lib/graphql/tracing/legacy_trace.rb +4 -61
  198. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  199. data/lib/graphql/tracing/new_relic_trace.rb +47 -54
  200. data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
  201. data/lib/graphql/tracing/notifications_trace.rb +183 -37
  202. data/lib/graphql/tracing/notifications_tracing.rb +2 -0
  203. data/lib/graphql/tracing/null_trace.rb +9 -0
  204. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  205. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  206. data/lib/graphql/tracing/perfetto_trace.rb +818 -0
  207. data/lib/graphql/tracing/platform_tracing.rb +1 -1
  208. data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
  209. data/lib/graphql/tracing/prometheus_trace.rb +73 -73
  210. data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
  211. data/lib/graphql/tracing/scout_trace.rb +32 -58
  212. data/lib/graphql/tracing/scout_tracing.rb +2 -0
  213. data/lib/graphql/tracing/sentry_trace.rb +64 -98
  214. data/lib/graphql/tracing/statsd_trace.rb +33 -45
  215. data/lib/graphql/tracing/statsd_tracing.rb +2 -0
  216. data/lib/graphql/tracing/trace.rb +111 -1
  217. data/lib/graphql/tracing.rb +31 -30
  218. data/lib/graphql/type_kinds.rb +2 -1
  219. data/lib/graphql/types/relay/connection_behaviors.rb +12 -2
  220. data/lib/graphql/types/relay/edge_behaviors.rb +11 -1
  221. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  222. data/lib/graphql/types.rb +18 -11
  223. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  224. data/lib/graphql/version.rb +1 -1
  225. data/lib/graphql.rb +64 -54
  226. metadata +197 -22
  227. data/lib/graphql/analysis/ast/analyzer.rb +0 -91
  228. data/lib/graphql/analysis/ast/field_usage.rb +0 -82
  229. data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
  230. data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
  231. data/lib/graphql/analysis/ast/query_complexity.rb +0 -182
  232. data/lib/graphql/analysis/ast/visitor.rb +0 -276
  233. data/lib/graphql/analysis/ast.rb +0 -94
  234. data/lib/graphql/backtrace/inspect_result.rb +0 -50
  235. data/lib/graphql/backtrace/trace.rb +0 -93
  236. data/lib/graphql/backtrace/tracer.rb +0 -80
  237. data/lib/graphql/language/token.rb +0 -34
  238. data/lib/graphql/schema/invalid_type_error.rb +0 -7
  239. data/lib/graphql/schema/null_mask.rb +0 -11
  240. 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: 543f8d4c101b53b2dce0d9b01d694a1ed0be3f8d22663cc33ddf40cce35da066
4
- data.tar.gz: 51c773a99b528550dde8b0bfa845fbd2871198a329903aa44e96319d805d3ef4
3
+ metadata.gz: 92559d0138e0b495e6bc3e571fb804c6ace53a9b60facf3711323bafe3f7970a
4
+ data.tar.gz: 1acd789d92c34070bbe768d4f390f58a540e99627e9d3171b25b423d2a0356f1
5
5
  SHA512:
6
- metadata.gz: f1aed999ba68c4020e5c63e3616d02a7b46b8626680182a67222a0e35636b12c1b1b024f99e84f4ac61bad06757c2599f331918240ffc6cac0cd060877bb4f7c
7
- data.tar.gz: 6180c9f78064cb20bc6b9f8fe15b3961371abe0f0462c4d4e306e830ed0864c0e2e4996aecc1cd1504a3341c969770265fa9497948f9f53e2a36f402a3ded93f
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 tipe, and adds the latter to the schema"
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 'namespaced_types',
21
+ class_option :namespaced_types,
22
22
  type: :boolean,
23
23
  required: false,
24
24
  default: false,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Resolvers
3
5
  class BaseResolver < GraphQL::Schema::Resolver
@@ -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
@@ -11,7 +11,7 @@ module Graphql
11
11
  class TypeGeneratorBase < Rails::Generators::NamedBase
12
12
  include Core
13
13
 
14
- class_option 'namespaced_types',
14
+ class_option :namespaced_types,
15
15
  type: :boolean,
16
16
  required: false,
17
17
  default: false,
@@ -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
- module AST
27
- class QueryDepth < Analyzer
28
- def initialize(query)
29
- @max_depth = 0
30
- @current_depth = 0
31
- @count_introspection_fields = query.schema.count_introspection_fields
32
- super
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
- def on_enter_field(node, parent, visitor)
36
- return if visitor.skipping? ||
37
- visitor.visiting_fragment_definition? ||
38
- (@count_introspection_fields == false && visitor.field_definition.introspection?)
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
- @current_depth += 1
41
- end
39
+ @current_depth += 1
40
+ end
42
41
 
43
- def on_leave_field(node, parent, visitor)
44
- return if visitor.skipping? ||
45
- visitor.visiting_fragment_definition? ||
46
- (@count_introspection_fields == false && visitor.field_definition.introspection?)
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
- if @max_depth < @current_depth
49
- @max_depth = @current_depth
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
- def result
55
- @max_depth
56
- end
53
+ def result
54
+ @max_depth
57
55
  end
58
56
  end
59
57
  end