graphql 1.9.6 → 1.9.7

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 (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