graphql 1.9.6 → 1.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +5 -5
  2. data/lib/generators/graphql/install_generator.rb +2 -1
  3. data/lib/generators/graphql/templates/base_field.erb +7 -0
  4. data/lib/graphql/analysis/ast/visitor.rb +5 -2
  5. data/lib/graphql/execution/multiplex.rb +5 -1
  6. data/lib/graphql/query.rb +6 -1
  7. data/lib/graphql/relay/array_connection.rb +1 -1
  8. data/lib/graphql/schema.rb +15 -1
  9. data/lib/graphql/schema/argument.rb +5 -1
  10. data/lib/graphql/schema/input_object.rb +21 -12
  11. data/lib/graphql/schema/introspection_system.rb +6 -1
  12. data/lib/graphql/schema/member/has_arguments.rb +4 -2
  13. data/lib/graphql/schema/resolver.rb +8 -3
  14. data/lib/graphql/schema/subscription.rb +22 -0
  15. data/lib/graphql/schema/timeout.rb +109 -0
  16. data/lib/graphql/types/relay/base_edge.rb +0 -3
  17. data/lib/graphql/upgrader/member.rb +148 -111
  18. data/lib/graphql/version.rb +1 -1
  19. data/readme.md +1 -1
  20. data/spec/fixtures/upgrader/mutation.original.rb +28 -0
  21. data/spec/fixtures/upgrader/mutation.transformed.rb +28 -0
  22. data/spec/graphql/analysis/ast_spec.rb +27 -0
  23. data/spec/graphql/execution/instrumentation_spec.rb +34 -6
  24. data/spec/graphql/execution/multiplex_spec.rb +11 -0
  25. data/spec/graphql/internal_representation/rewrite_spec.rb +6 -1
  26. data/spec/graphql/schema/input_object_spec.rb +56 -7
  27. data/spec/graphql/schema/introspection_system_spec.rb +24 -0
  28. data/spec/graphql/schema/subscription_spec.rb +65 -0
  29. data/spec/graphql/schema/timeout_spec.rb +206 -0
  30. data/spec/integration/mongoid/star_trek/schema.rb +1 -2
  31. data/spec/integration/rails/graphql/input_object_spec.rb +19 -0
  32. data/spec/integration/rails/graphql/relay/array_connection_spec.rb +47 -28
  33. data/spec/integration/rails/graphql/schema_spec.rb +18 -0
  34. data/spec/integration/tmp/app/graphql/types/date_type.rb +14 -0
  35. data/spec/integration/tmp/dummy/Gemfile +50 -0
  36. data/spec/integration/tmp/dummy/README.md +24 -0
  37. data/spec/integration/tmp/dummy/Rakefile +6 -0
  38. data/spec/integration/tmp/dummy/app/assets/config/manifest.js +3 -0
  39. data/spec/integration/tmp/dummy/app/assets/javascripts/application.js +16 -0
  40. data/spec/integration/tmp/dummy/app/assets/javascripts/cable.js +13 -0
  41. data/spec/integration/tmp/dummy/app/assets/stylesheets/application.css +15 -0
  42. data/spec/integration/tmp/dummy/app/channels/application_cable/channel.rb +5 -0
  43. data/spec/integration/tmp/dummy/app/channels/application_cable/connection.rb +5 -0
  44. data/spec/integration/tmp/dummy/app/controllers/application_controller.rb +4 -0
  45. data/spec/integration/tmp/dummy/app/controllers/graphql_controller.rb +44 -0
  46. data/spec/integration/tmp/dummy/app/helpers/application_helper.rb +3 -0
  47. data/spec/integration/tmp/dummy/app/jobs/application_job.rb +3 -0
  48. data/spec/integration/tmp/dummy/app/mailers/application_mailer.rb +5 -0
  49. data/spec/integration/tmp/dummy/app/mydirectory/dummy_schema.rb +5 -0
  50. data/spec/integration/tmp/dummy/app/mydirectory/mutations/update_name.rb +15 -0
  51. data/spec/integration/tmp/dummy/app/mydirectory/types/base_enum.rb +5 -0
  52. data/spec/integration/tmp/dummy/app/mydirectory/types/base_input_object.rb +5 -0
  53. data/spec/integration/tmp/dummy/app/mydirectory/types/base_interface.rb +6 -0
  54. data/spec/integration/tmp/dummy/app/mydirectory/types/base_object.rb +5 -0
  55. data/spec/integration/tmp/dummy/app/mydirectory/types/base_scalar.rb +5 -0
  56. data/spec/integration/tmp/dummy/app/mydirectory/types/base_union.rb +5 -0
  57. data/spec/integration/tmp/dummy/app/mydirectory/types/mutation_type.rb +12 -0
  58. data/spec/integration/tmp/dummy/app/mydirectory/types/query_type.rb +14 -0
  59. data/spec/integration/tmp/dummy/app/views/layouts/application.html.erb +14 -0
  60. data/spec/integration/tmp/dummy/app/views/layouts/mailer.html.erb +13 -0
  61. data/spec/integration/tmp/dummy/app/views/layouts/mailer.text.erb +1 -0
  62. data/spec/integration/tmp/dummy/bin/bundle +3 -0
  63. data/spec/integration/tmp/dummy/bin/rails +4 -0
  64. data/spec/integration/tmp/dummy/bin/rake +4 -0
  65. data/spec/integration/tmp/dummy/bin/setup +34 -0
  66. data/spec/integration/tmp/dummy/bin/update +29 -0
  67. data/spec/integration/tmp/dummy/config.ru +5 -0
  68. data/spec/integration/tmp/dummy/config/application.rb +26 -0
  69. data/spec/integration/tmp/dummy/config/boot.rb +4 -0
  70. data/spec/integration/tmp/dummy/config/cable.yml +9 -0
  71. data/spec/integration/tmp/dummy/config/environment.rb +6 -0
  72. data/spec/integration/tmp/dummy/config/environments/development.rb +52 -0
  73. data/spec/integration/tmp/dummy/config/environments/production.rb +84 -0
  74. data/spec/integration/tmp/dummy/config/environments/test.rb +43 -0
  75. data/spec/integration/tmp/dummy/config/initializers/application_controller_renderer.rb +9 -0
  76. data/spec/integration/tmp/dummy/config/initializers/assets.rb +12 -0
  77. data/spec/integration/tmp/dummy/config/initializers/backtrace_silencers.rb +8 -0
  78. data/spec/integration/tmp/dummy/config/initializers/cookies_serializer.rb +6 -0
  79. data/spec/integration/tmp/dummy/config/initializers/filter_parameter_logging.rb +5 -0
  80. data/spec/integration/tmp/dummy/config/initializers/inflections.rb +17 -0
  81. data/spec/integration/tmp/dummy/config/initializers/mime_types.rb +5 -0
  82. data/spec/integration/tmp/dummy/config/initializers/new_framework_defaults.rb +24 -0
  83. data/spec/integration/tmp/dummy/config/initializers/session_store.rb +4 -0
  84. data/spec/integration/tmp/dummy/config/initializers/wrap_parameters.rb +10 -0
  85. data/spec/integration/tmp/dummy/config/locales/en.yml +23 -0
  86. data/spec/integration/tmp/dummy/config/puma.rb +48 -0
  87. data/spec/integration/tmp/dummy/config/routes.rb +9 -0
  88. data/spec/integration/tmp/dummy/config/secrets.yml +22 -0
  89. data/spec/integration/tmp/dummy/db/seeds.rb +8 -0
  90. data/spec/integration/tmp/dummy/log/test.log +0 -0
  91. data/spec/integration/tmp/dummy/public/404.html +67 -0
  92. data/spec/integration/tmp/dummy/public/422.html +67 -0
  93. data/spec/integration/tmp/dummy/public/500.html +66 -0
  94. data/spec/integration/tmp/dummy/public/apple-touch-icon-precomposed.png +0 -0
  95. data/spec/integration/tmp/dummy/public/apple-touch-icon.png +0 -0
  96. data/spec/integration/tmp/dummy/public/favicon.ico +0 -0
  97. data/spec/integration/tmp/dummy/public/robots.txt +5 -0
  98. data/spec/integration/tmp/dummy/test/test_helper.rb +8 -0
  99. data/spec/support/jazz.rb +6 -0
  100. data/spec/support/star_wars/schema.rb +1 -2
  101. metadata +171 -6
  102. data/spec/integration/tmp/app/graphql/types/bird_type.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8bc71e9155287bd634009a90a72adcc1e96c6e82
4
- data.tar.gz: 3ce70233bd4420c72d2a4c7189d6a7c7edd5a75a
2
+ SHA256:
3
+ metadata.gz: bc1010a9a70360cc6b3d6dcb13db9d35c207179686f503d01d5904c2655f9ae2
4
+ data.tar.gz: 510f939a5c9dd9b1a66610e7d526aa84e9ae2b494f76a9a07973aef0b24e6cde
5
5
  SHA512:
6
- metadata.gz: 1c536685b8bbab3e649377e1ea8f30c5fd5757b7209ff488fe64b4bf8be0dc5b90c74ac877ae39de3149e8557387ef321299cdfd788de3ab2b0090136cb954e1
7
- data.tar.gz: 990776aff9a606378d3a61f3d9064f1e9257a97dbb4e2e0194c0306d5dbeaa4961583262e42bd0957464d1b9ee06c6ecc18b380a7d9061f89f4f6dcb5136773f
6
+ metadata.gz: bebd9ccec78e0ba8e2d2a13d351a33e9a169261faf954bc50710f80c4ad6d555e495761681e0691213a9ed409b3057f77118f91e73e577fdd988759e4c7eb743
7
+ data.tar.gz: d458a432047fa847b775f8ff5558084eb2cc845a5277d4f47643483de5f9049d2b19c8c7831058c79b28bc962000d8f13c5201a7396169fa81bf080be8f12e18
@@ -13,6 +13,7 @@ module Graphql
13
13
  # - graphql/
14
14
  # - resolvers/
15
15
  # - types/
16
+ # - base_field.rb
16
17
  # - base_enum.rb
17
18
  # - base_input_object.rb
18
19
  # - base_interface.rb
@@ -93,7 +94,7 @@ module Graphql
93
94
  create_dir("#{options[:directory]}/types")
94
95
  template("schema.erb", schema_file_path)
95
96
 
96
- ["base_object", "base_enum", "base_input_object", "base_interface", "base_scalar", "base_union"].each do |base_type|
97
+ ["base_object", "base_field", "base_enum", "base_input_object", "base_interface", "base_scalar", "base_union"].each do |base_type|
97
98
  template("#{base_type}.erb", "#{options[:directory]}/types/#{base_type}.rb")
98
99
  end
99
100
 
@@ -0,0 +1,7 @@
1
+ module Types
2
+ class BaseField < GraphQL::Schema::Field
3
+ def resolve_field(obj, args, ctx)
4
+ resolve(obj, args, ctx)
5
+ end
6
+ end
7
+ end
@@ -220,8 +220,11 @@ module GraphQL
220
220
 
221
221
  # @return [GraphQL::Argument, nil] The most-recently-entered GraphQL::Argument, if currently inside one
222
222
  def argument_definition
223
- # Don't get the _last_ one because that's the current one.
224
- # Get the second-to-last one, which is the parent of the current one.
223
+ @argument_definitions.last
224
+ end
225
+
226
+ # @return [GraphQL::Argument, nil] The previous GraphQL argument
227
+ def previous_argument_definition
225
228
  @argument_definitions[-2]
226
229
  end
227
230
 
@@ -175,7 +175,11 @@ module GraphQL
175
175
  schema = multiplex.schema
176
176
  multiplex_analyzers = schema.multiplex_analyzers
177
177
  if multiplex.max_complexity
178
- multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity.new(multiplex.max_complexity)]
178
+ multiplex_analyzers += if schema.using_ast_analysis?
179
+ [GraphQL::Analysis::AST::MaxQueryComplexity]
180
+ else
181
+ [GraphQL::Analysis::MaxQueryComplexity.new(multiplex.max_complexity)]
182
+ end
179
183
  end
180
184
 
181
185
  schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)
data/lib/graphql/query.rb CHANGED
@@ -44,7 +44,12 @@ module GraphQL
44
44
 
45
45
  # @return [GraphQL::Language::Nodes::Document]
46
46
  def document
47
- with_prepared_ast { @document }
47
+ # It's ok if this hasn't been assigned yet
48
+ if @query_string || @document
49
+ with_prepared_ast { @document }
50
+ else
51
+ nil
52
+ end
48
53
  end
49
54
 
50
55
  def inspect
@@ -13,7 +13,7 @@ module GraphQL
13
13
  sliced_nodes.count > first
14
14
  elsif GraphQL::Relay::ConnectionType.bidirectional_pagination && before
15
15
  # The original array is longer than the `before` index
16
- index_from_cursor(before) < nodes.length
16
+ index_from_cursor(before) < nodes.length + 1
17
17
  else
18
18
  false
19
19
  end
@@ -11,6 +11,7 @@ require "graphql/schema/middleware_chain"
11
11
  require "graphql/schema/null_mask"
12
12
  require "graphql/schema/possible_types"
13
13
  require "graphql/schema/rescue_middleware"
14
+ require "graphql/schema/timeout"
14
15
  require "graphql/schema/timeout_middleware"
15
16
  require "graphql/schema/traversal"
16
17
  require "graphql/schema/type_expression"
@@ -90,6 +91,7 @@ module GraphQL
90
91
  :object_from_id, :id_from_object,
91
92
  :default_mask,
92
93
  :cursor_encoder,
94
+ disable_introspection_entry_points: ->(schema) { schema.disable_introspection_entry_points = true },
93
95
  directives: ->(schema, directives) { schema.directives = directives.reduce({}) { |m, d| m[d.name] = d; m } },
94
96
  directive: ->(schema, directive) { schema.directives[directive.graphql_name] = directive },
95
97
  instrument: ->(schema, type, instrumenter, after_built_ins: false) {
@@ -110,6 +112,8 @@ module GraphQL
110
112
  rescue_from: ->(schema, err_class, &block) { schema.rescue_from(err_class, &block) },
111
113
  tracer: ->(schema, tracer) { schema.tracers.push(tracer) }
112
114
 
115
+ ensure_defined :introspection_system
116
+
113
117
  attr_accessor \
114
118
  :query, :mutation, :subscription,
115
119
  :query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
@@ -140,6 +144,9 @@ module GraphQL
140
144
  # @return [Class] Instantiated for each query
141
145
  attr_accessor :context_class
142
146
 
147
+ # [Boolean] True if this object disables the introspection entry point fields
148
+ attr_accessor :disable_introspection_entry_points
149
+
143
150
  class << self
144
151
  attr_writer :default_execution_strategy
145
152
  end
@@ -186,6 +193,7 @@ module GraphQL
186
193
  @introspection_system = nil
187
194
  @interpreter = false
188
195
  @error_bubbling = false
196
+ @disable_introspection_entry_points = false
189
197
  end
190
198
 
191
199
  # @return [Boolean] True if using the new {GraphQL::Execution::Interpreter}
@@ -712,7 +720,8 @@ module GraphQL
712
720
  :subscriptions,
713
721
  :union_memberships,
714
722
  :get_field, :root_types, :references_to, :type_from_ast,
715
- :possible_types
723
+ :possible_types,
724
+ :disable_introspection_entry_points=
716
725
 
717
726
  def graphql_definition
718
727
  @graphql_definition ||= to_graphql
@@ -737,6 +746,7 @@ module GraphQL
737
746
  schema_defn.max_depth = max_depth
738
747
  schema_defn.default_max_page_size = default_max_page_size
739
748
  schema_defn.orphan_types = orphan_types
749
+ schema_defn.disable_introspection_entry_points = @disable_introspection_entry_points
740
750
 
741
751
  prepped_dirs = {}
742
752
  directives.each { |k, v| prepped_dirs[k] = v.graphql_definition}
@@ -887,6 +897,10 @@ module GraphQL
887
897
  end
888
898
  end
889
899
 
900
+ def disable_introspection_entry_points
901
+ @disable_introspection_entry_points = true
902
+ end
903
+
890
904
  def orphan_types(*new_orphan_types)
891
905
  if new_orphan_types.any?
892
906
  @orphan_types = new_orphan_types.flatten
@@ -21,6 +21,9 @@ module GraphQL
21
21
  # @return [Symbol] This argument's name in Ruby keyword arguments
22
22
  attr_reader :keyword
23
23
 
24
+ # @return [Class, Module, nil] If this argument should load an application object, this is the type of object to load
25
+ attr_reader :loads
26
+
24
27
  # @param arg_name [Symbol]
25
28
  # @param type_expr
26
29
  # @param desc [String]
@@ -30,7 +33,7 @@ module GraphQL
30
33
  # @param as [Symbol] Override the keyword name when passed to a method
31
34
  # @param prepare [Symbol] A method to call to transform this argument's valuebefore sending it to field resolution
32
35
  # @param camelize [Boolean] if true, the name will be camelized when building the schema
33
- def initialize(arg_name = nil, type_expr = nil, desc = nil, required:, type: nil, name: nil, description: nil, default_value: NO_DEFAULT, as: nil, camelize: true, prepare: nil, owner:, &definition_block)
36
+ def initialize(arg_name = nil, type_expr = nil, desc = nil, required:, type: nil, name: nil, loads: nil, description: nil, default_value: NO_DEFAULT, as: nil, camelize: true, prepare: nil, owner:, &definition_block)
34
37
  arg_name ||= name
35
38
  name_str = camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s
36
39
  @name = name_str.freeze
@@ -40,6 +43,7 @@ module GraphQL
40
43
  @default_value = default_value
41
44
  @owner = owner
42
45
  @as = as
46
+ @loads = loads
43
47
  @keyword = as || Schema::Member::BuildType.underscore(@name).to_sym
44
48
  @prepare = prepare
45
49
 
@@ -21,6 +21,17 @@ module GraphQL
21
21
  self.class.arguments.each do |name, arg_defn|
22
22
  @arguments_by_keyword[arg_defn.keyword] = arg_defn
23
23
  ruby_kwargs_key = arg_defn.keyword
24
+ loads = arg_defn.loads
25
+
26
+ if @ruby_style_hash.key?(ruby_kwargs_key) && loads
27
+ value = @ruby_style_hash[ruby_kwargs_key]
28
+ @ruby_style_hash[ruby_kwargs_key] = if arg_defn.type.list?
29
+ GraphQL::Execution::Lazy.all(value.map { |val| load_application_object(arg_defn, loads, val) })
30
+ else
31
+ load_application_object(arg_defn, loads, value)
32
+ end
33
+ end
34
+
24
35
  if @ruby_style_hash.key?(ruby_kwargs_key) && arg_defn.prepare
25
36
  @ruby_style_hash[ruby_kwargs_key] = arg_defn.prepare_value(self, @ruby_style_hash[ruby_kwargs_key])
26
37
  end
@@ -42,6 +53,10 @@ module GraphQL
42
53
  end
43
54
  end
44
55
 
56
+ def to_hash
57
+ to_h
58
+ end
59
+
45
60
  def unwrap_value(value)
46
61
  case value
47
62
  when Array
@@ -83,20 +98,14 @@ module GraphQL
83
98
  # @return [Class<GraphQL::Arguments>]
84
99
  attr_accessor :arguments_class
85
100
 
86
- def argument(name, type, *rest, loads: nil, **kwargs, &block)
87
- argument_defn = super(*argument_with_loads(name, type, *rest, loads: loads, **kwargs, &block))
101
+ def argument(*args, **kwargs, &block)
102
+ # Translate `loads:` to `as:` if needed`
103
+ *args, kwargs = argument_with_loads(*args, **kwargs, &block)
104
+ argument_defn = super(*args, **kwargs, &block)
88
105
  # Add a method access
89
106
  method_name = argument_defn.keyword
90
107
  define_method(method_name) do
91
- value = @ruby_style_hash[method_name]
92
- argument = @arguments_by_keyword[method_name]
93
- if loads && argument_defn.type.list?
94
- GraphQL::Execution::Lazy.all(value.map { |val| load_application_object(argument, loads, val) })
95
- elsif loads
96
- load_application_object(argument, loads, value)
97
- else
98
- value
99
- end
108
+ self[method_name]
100
109
  end
101
110
  end
102
111
 
@@ -122,4 +131,4 @@ module GraphQL
122
131
  end
123
132
  end
124
133
  end
125
- end
134
+ end
@@ -18,7 +18,12 @@ module GraphQL
18
18
  @input_value_type = load_constant(:InputValueType).to_graphql
19
19
  @type_kind_enum = load_constant(:TypeKindEnum).to_graphql
20
20
  @directive_location_enum = load_constant(:DirectiveLocationEnum).to_graphql
21
- @entry_point_fields = get_fields_from_class(class_sym: :EntryPoints)
21
+ @entry_point_fields =
22
+ if schema.disable_introspection_entry_points
23
+ {}
24
+ else
25
+ get_fields_from_class(class_sym: :EntryPoints)
26
+ end
22
27
  @dynamic_fields = get_fields_from_class(class_sym: :DynamicFields)
23
28
  end
24
29
 
@@ -13,8 +13,10 @@ module GraphQL
13
13
  cls.include(ArgumentObjectLoader)
14
14
  end
15
15
 
16
- def argument_with_loads(name, type, *rest, loads: nil, **kwargs)
16
+ def argument_with_loads(*args, **kwargs)
17
+ loads = kwargs[:loads]
17
18
  if loads
19
+ name = args[0]
18
20
  name_as_string = name.to_s
19
21
 
20
22
  inferred_arg_name = case name_as_string
@@ -31,7 +33,7 @@ module GraphQL
31
33
  kwargs[:as] ||= inferred_arg_name
32
34
  end
33
35
 
34
- return [name, type, *rest, **kwargs]
36
+ return [*args, **kwargs]
35
37
  end
36
38
 
37
39
  # @see {GraphQL::Schema::Argument#initialize} for parameters
@@ -255,8 +255,11 @@ module GraphQL
255
255
  # also add some preparation hook methods which will be used for this argument
256
256
  # @see {GraphQL::Schema::Argument#initialize} for the signature
257
257
  def argument(name, type, *rest, loads: nil, **kwargs, &block)
258
- arg_defn = super(*argument_with_loads(name, type, *rest, loads: loads, **kwargs, &block))
259
-
258
+ *args, kwargs = argument_with_loads(name, type, *rest, loads: loads, **kwargs, &block)
259
+ # Short-circuit the InputObject's own `loads:` implementation
260
+ # so that we can support `#load_{x}` methods below.
261
+ kwargs.delete(:loads)
262
+ arg_defn = super(*args, **kwargs)
260
263
  own_arguments_loads_as_type[arg_defn.keyword] = loads if loads
261
264
 
262
265
  if loads && arg_defn.type.list?
@@ -264,7 +267,9 @@ module GraphQL
264
267
  def load_#{arg_defn.keyword}(values)
265
268
  argument = @arguments_by_keyword[:#{arg_defn.keyword}]
266
269
  lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
267
- GraphQL::Execution::Lazy.all(values.map { |value| load_application_object(argument, lookup_as_type, value) })
270
+ context.schema.after_lazy(values) do |values2|
271
+ GraphQL::Execution::Lazy.all(values2.map { |value| load_application_object(argument, lookup_as_type, value) })
272
+ end
268
273
  end
269
274
  RUBY
270
275
  elsif loads
@@ -92,6 +92,28 @@ module GraphQL
92
92
  def unsubscribe
93
93
  raise UnsubscribedError
94
94
  end
95
+
96
+ # Call this method to provide a new subscription_scope; OR
97
+ # call it without an argument to get the subscription_scope
98
+ # @param new_scope [Symbol]
99
+ # @return [Symbol]
100
+ READING_SCOPE = ::Object.new
101
+ def self.subscription_scope(new_scope = READING_SCOPE)
102
+ if new_scope != READING_SCOPE
103
+ @subscription_scope = new_scope
104
+ elsif defined?(@subscription_scope)
105
+ @subscription_scope
106
+ else
107
+ find_inherited_method(:subscription_scope, nil)
108
+ end
109
+ end
110
+
111
+ # Overriding Resolver#field_options to include subscription_scope
112
+ def self.field_options
113
+ super.merge(
114
+ subscription_scope: subscription_scope
115
+ )
116
+ end
95
117
  end
96
118
  end
97
119
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ # This plugin will stop resolving new fields after `max_seconds` have elapsed.
6
+ # After the time has passed, any remaining fields will be `nil`, with errors added
7
+ # to the `errors` key. Any already-resolved fields will be in the `data` key, so
8
+ # you'll get a partial response.
9
+ #
10
+ # You can subclass `GraphQL::Schema::Timeout` and override the `handle_timeout` method
11
+ # to provide custom logic when a timeout error occurs.
12
+ #
13
+ # Note that this will stop a query _in between_ field resolutions, but
14
+ # it doesn't interrupt long-running `resolve` functions. Be sure to use
15
+ # timeout options for external connections. For more info, see
16
+ # www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/
17
+ #
18
+ # @example Stop resolving fields after 2 seconds
19
+ # class MySchema < GraphQL::Schema
20
+ # use GraphQL::Schema::Timeout, max_seconds: 2
21
+ # end
22
+ #
23
+ # @example Notifying Bugsnag and logging a timeout
24
+ # class MyTimeout < GraphQL::Schema::Timeout
25
+ # def handle_timeout(error, query)
26
+ # Rails.logger.warn("GraphQL Timeout: #{error.message}: #{query.query_string}")
27
+ # Bugsnag.notify(error, {query_string: query.query_string})
28
+ # end
29
+ # end
30
+ #
31
+ # class MySchema < GraphQL::Schema
32
+ # use MyTimeout, max_seconds: 2
33
+ # end
34
+ #
35
+ class Timeout
36
+ attr_reader :max_seconds
37
+
38
+ def self.use(schema, **options)
39
+ tracer = new(**options)
40
+ schema.tracer(tracer)
41
+ end
42
+
43
+ # @param max_seconds [Numeric] how many seconds the query should be allowed to resolve new fields
44
+ def initialize(max_seconds:)
45
+ @max_seconds = max_seconds
46
+ end
47
+
48
+ def trace(key, data)
49
+ case key
50
+ when 'execute_multiplex'
51
+ timeout_state = {
52
+ timeout_at: Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + max_seconds * 1000,
53
+ timed_out: false
54
+ }
55
+
56
+ data.fetch(:multiplex).queries.each do |query|
57
+ query.context.namespace(self.class)[:state] = timeout_state
58
+ end
59
+
60
+ yield
61
+ when 'execute_field', 'execute_field_lazy'
62
+ query = data[:context] ? data.fetch(:context).query : data.fetch(:query)
63
+ timeout_state = query.context.namespace(self.class).fetch(:state)
64
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at)
65
+ error = if data[:context]
66
+ context = data.fetch(:context)
67
+ GraphQL::Schema::Timeout::TimeoutError.new(context.parent_type, context.field)
68
+ else
69
+ field = data.fetch(:field)
70
+ GraphQL::Schema::Timeout::TimeoutError.new(field.owner, field)
71
+ end
72
+
73
+ # Only invoke the timeout callback for the first timeout
74
+ unless timeout_state[:timed_out]
75
+ timeout_state[:timed_out] = true
76
+ handle_timeout(error, query)
77
+ end
78
+
79
+ error
80
+ else
81
+ yield
82
+ end
83
+ else
84
+ yield
85
+ end
86
+ end
87
+
88
+ # Invoked when a query times out.
89
+ # @param error [GraphQL::Schema::Timeout::TimeoutError]
90
+ # @param query [GraphQL::Error]
91
+ def handle_timeout(error, query)
92
+ # override to do something interesting
93
+ end
94
+
95
+ # This error is raised when a query exceeds `max_seconds`.
96
+ # Since it's a child of {GraphQL::ExecutionError},
97
+ # its message will be added to the response's `errors` key.
98
+ #
99
+ # To raise an error that will stop query resolution, use a custom block
100
+ # to take this error and raise a new one which _doesn't_ descend from {GraphQL::ExecutionError},
101
+ # such as `RuntimeError`.
102
+ class TimeoutError < GraphQL::ExecutionError
103
+ def initialize(parent_type, field)
104
+ super("Timeout on #{parent_type.graphql_name}.#{field.graphql_name}")
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end