graphql 2.3.7 → 2.4.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +46 -0
  3. data/lib/generators/graphql/orm_mutations_base.rb +1 -1
  4. data/lib/generators/graphql/templates/base_resolver.erb +2 -0
  5. data/lib/generators/graphql/type_generator.rb +1 -1
  6. data/lib/graphql/analysis/field_usage.rb +1 -1
  7. data/lib/graphql/analysis/query_complexity.rb +3 -3
  8. data/lib/graphql/analysis/visitor.rb +8 -7
  9. data/lib/graphql/analysis.rb +4 -4
  10. data/lib/graphql/autoload.rb +38 -0
  11. data/lib/graphql/current.rb +52 -0
  12. data/lib/graphql/dataloader/async_dataloader.rb +7 -6
  13. data/lib/graphql/dataloader/source.rb +7 -4
  14. data/lib/graphql/dataloader.rb +40 -19
  15. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  16. data/lib/graphql/execution/interpreter/resolve.rb +13 -9
  17. data/lib/graphql/execution/interpreter/runtime.rb +35 -31
  18. data/lib/graphql/execution/interpreter.rb +6 -4
  19. data/lib/graphql/execution/lookahead.rb +18 -11
  20. data/lib/graphql/introspection/directive_type.rb +1 -1
  21. data/lib/graphql/introspection/entry_points.rb +2 -2
  22. data/lib/graphql/introspection/field_type.rb +1 -1
  23. data/lib/graphql/introspection/schema_type.rb +6 -11
  24. data/lib/graphql/introspection/type_type.rb +5 -5
  25. data/lib/graphql/invalid_null_error.rb +1 -1
  26. data/lib/graphql/language/cache.rb +13 -0
  27. data/lib/graphql/language/comment.rb +18 -0
  28. data/lib/graphql/language/document_from_schema_definition.rb +62 -34
  29. data/lib/graphql/language/lexer.rb +18 -15
  30. data/lib/graphql/language/nodes.rb +24 -16
  31. data/lib/graphql/language/parser.rb +14 -1
  32. data/lib/graphql/language/printer.rb +31 -15
  33. data/lib/graphql/language/sanitized_printer.rb +1 -1
  34. data/lib/graphql/language.rb +6 -6
  35. data/lib/graphql/pagination/connection.rb +1 -1
  36. data/lib/graphql/query/context/scoped_context.rb +1 -1
  37. data/lib/graphql/query/context.rb +13 -6
  38. data/lib/graphql/query/null_context.rb +3 -5
  39. data/lib/graphql/query/variable_validation_error.rb +1 -1
  40. data/lib/graphql/query.rb +72 -18
  41. data/lib/graphql/railtie.rb +7 -0
  42. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  43. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  44. data/lib/graphql/rubocop.rb +2 -0
  45. data/lib/graphql/schema/addition.rb +2 -1
  46. data/lib/graphql/schema/always_visible.rb +6 -2
  47. data/lib/graphql/schema/argument.rb +14 -1
  48. data/lib/graphql/schema/build_from_definition.rb +9 -1
  49. data/lib/graphql/schema/directive/flagged.rb +2 -2
  50. data/lib/graphql/schema/directive.rb +1 -1
  51. data/lib/graphql/schema/enum.rb +71 -23
  52. data/lib/graphql/schema/enum_value.rb +10 -2
  53. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  54. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  55. data/lib/graphql/schema/field.rb +102 -47
  56. data/lib/graphql/schema/field_extension.rb +1 -1
  57. data/lib/graphql/schema/has_single_input_argument.rb +5 -2
  58. data/lib/graphql/schema/input_object.rb +90 -39
  59. data/lib/graphql/schema/interface.rb +22 -5
  60. data/lib/graphql/schema/introspection_system.rb +5 -16
  61. data/lib/graphql/schema/loader.rb +1 -1
  62. data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
  63. data/lib/graphql/schema/member/has_arguments.rb +36 -23
  64. data/lib/graphql/schema/member/has_directives.rb +3 -3
  65. data/lib/graphql/schema/member/has_fields.rb +26 -6
  66. data/lib/graphql/schema/member/has_interfaces.rb +4 -4
  67. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  68. data/lib/graphql/schema/member/has_validators.rb +1 -1
  69. data/lib/graphql/schema/object.rb +8 -0
  70. data/lib/graphql/schema/printer.rb +1 -0
  71. data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
  72. data/lib/graphql/schema/resolver.rb +12 -14
  73. data/lib/graphql/schema/subscription.rb +2 -2
  74. data/lib/graphql/schema/type_expression.rb +2 -2
  75. data/lib/graphql/schema/union.rb +1 -1
  76. data/lib/graphql/schema/validator/all_validator.rb +62 -0
  77. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  78. data/lib/graphql/schema/validator.rb +3 -1
  79. data/lib/graphql/schema/visibility/migration.rb +188 -0
  80. data/lib/graphql/schema/visibility/profile.rb +359 -0
  81. data/lib/graphql/schema/visibility/visit.rb +190 -0
  82. data/lib/graphql/schema/visibility.rb +294 -0
  83. data/lib/graphql/schema/warden.rb +179 -16
  84. data/lib/graphql/schema.rb +348 -94
  85. data/lib/graphql/static_validation/base_visitor.rb +6 -5
  86. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  87. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  88. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
  89. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +3 -2
  90. data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
  91. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  92. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +12 -2
  93. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +2 -2
  94. data/lib/graphql/static_validation/rules/fields_will_merge.rb +8 -7
  95. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  96. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +12 -2
  97. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  98. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  99. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
  100. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  101. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +4 -4
  102. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
  103. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +1 -1
  104. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
  105. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  106. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
  107. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  108. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +11 -2
  109. data/lib/graphql/static_validation/validation_context.rb +18 -2
  110. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +3 -2
  111. data/lib/graphql/subscriptions/broadcast_analyzer.rb +10 -4
  112. data/lib/graphql/subscriptions/event.rb +1 -1
  113. data/lib/graphql/subscriptions/serialize.rb +2 -0
  114. data/lib/graphql/subscriptions.rb +6 -4
  115. data/lib/graphql/testing/helpers.rb +10 -6
  116. data/lib/graphql/tracing/notifications_trace.rb +2 -2
  117. data/lib/graphql/types/relay/connection_behaviors.rb +12 -2
  118. data/lib/graphql/types/relay/edge_behaviors.rb +11 -1
  119. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  120. data/lib/graphql/types.rb +18 -11
  121. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  122. data/lib/graphql/version.rb +1 -1
  123. data/lib/graphql.rb +53 -45
  124. metadata +31 -8
  125. data/lib/graphql/language/token.rb +0 -34
  126. data/lib/graphql/schema/invalid_type_error.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13269b31415c3c837e80ec0097b8162c65035119f16860a18012b15e71ace318
4
- data.tar.gz: a3d1a936954352bd4ac46aab7f682377f3c219c477db32a65eb414d04655f099
3
+ metadata.gz: 69f8b4084adf0530a973073bcacf76e536975b385e4927525504b648762e496e
4
+ data.tar.gz: ef8e532575d445e0e96c1e5cccb2f6518d4a8759127ba8f27fd4d2aca4ef877a
5
5
  SHA512:
6
- metadata.gz: 721413cdc90187b7348514ea90d65f25a22a88551c3665dee7ff7f5d986b515f535bb147fedc72fbee4942ac5fdea2f9c1ffb38f5751706a98cecef98e7f94e6
7
- data.tar.gz: 4a1b47909ffbc71665ee2451002da4e2633dd010775b49e66e7a98068b73e60e5dc3c37e2491ee10f4f52bacc24d0c38f19d31a2cc9f436e8af2ebd6fac73d03
6
+ metadata.gz: c9522e680f06a1810e019c64656fba5dd7d7d95e098f136e2f656837c928829eab7a1be5656c7c4c9fd68bf8f262ca9b7ed415d87c12997dc613c9f600e8994b
7
+ data.tar.gz: 99a5a17272d80d555c98f8f091799e5b84656bfd42310e79d64b65140c7b2786e9f23f4759dfefb5b09c8ffc913349bfd0db3bcff5f6475087b350122aadd066
@@ -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
@@ -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,
@@ -72,7 +72,7 @@ module GraphQL
72
72
  end
73
73
 
74
74
  def extract_deprecated_enum_value(enum_type, value)
75
- enum_value = @query.warden.enum_values(enum_type).find { |ev| ev.value == value }
75
+ enum_value = @query.types.enum_values(enum_type).find { |ev| ev.value == value }
76
76
  if enum_value&.deprecation_reason
77
77
  @used_deprecated_enum_values << enum_value.path
78
78
  end
@@ -98,7 +98,7 @@ module GraphQL
98
98
  possible_scope_types.keys.each do |possible_scope_type|
99
99
  next unless possible_scope_type.kind.abstract?
100
100
 
101
- query.possible_types(possible_scope_type).each do |impl_type|
101
+ query.types.possible_types(possible_scope_type).each do |impl_type|
102
102
  possible_scope_types[impl_type] ||= true
103
103
  end
104
104
  possible_scope_types.delete(possible_scope_type)
@@ -123,8 +123,8 @@ module GraphQL
123
123
  def types_intersect?(query, a, b)
124
124
  return true if a == b
125
125
 
126
- a_types = query.possible_types(a)
127
- query.possible_types(b).any? { |t| a_types.include?(t) }
126
+ a_types = query.types.possible_types(a)
127
+ query.types.possible_types(b).any? { |t| a_types.include?(t) }
128
128
  end
129
129
 
130
130
  # A hook which is called whenever a field's max complexity is calculated.
@@ -21,6 +21,7 @@ module GraphQL
21
21
  @rescued_errors = []
22
22
  @query = query
23
23
  @schema = query.schema
24
+ @types = query.types
24
25
  @response_path = []
25
26
  @skip_stack = [false]
26
27
  super(query.selected_operation)
@@ -131,7 +132,7 @@ module GraphQL
131
132
  @response_path.push(node.alias || node.name)
132
133
  parent_type = @object_types.last
133
134
  # This could be nil if the previous field wasn't found:
134
- field_definition = parent_type && @schema.get_field(parent_type, node.name, @query.context)
135
+ field_definition = parent_type && @types.field(parent_type, node.name)
135
136
  @field_definitions.push(field_definition)
136
137
  if !field_definition.nil?
137
138
  next_object_type = field_definition.type.unwrap
@@ -167,14 +168,14 @@ module GraphQL
167
168
  argument_defn = if (arg = @argument_definitions.last)
168
169
  arg_type = arg.type.unwrap
169
170
  if arg_type.kind.input_object?
170
- arg_type.get_argument(node.name, @query.context)
171
+ @types.argument(arg_type, node.name)
171
172
  else
172
173
  nil
173
174
  end
174
175
  elsif (directive_defn = @directive_definitions.last)
175
- directive_defn.get_argument(node.name, @query.context)
176
+ @types.argument(directive_defn, node.name)
176
177
  elsif (field_defn = @field_definitions.last)
177
- field_defn.get_argument(node.name, @query.context)
178
+ @types.argument(field_defn, node.name)
178
179
  else
179
180
  nil
180
181
  end
@@ -245,7 +246,7 @@ module GraphQL
245
246
  fragment_def = query.fragments[fragment_spread.name]
246
247
 
247
248
  object_type = if fragment_def.type
248
- @query.warden.get_type(fragment_def.type.name)
249
+ @types.type(fragment_def.type.name)
249
250
  else
250
251
  object_types.last
251
252
  end
@@ -263,12 +264,12 @@ module GraphQL
263
264
 
264
265
  def skip?(ast_node)
265
266
  dir = ast_node.directives
266
- dir.any? && !GraphQL::Execution::DirectiveChecks.include?(dir, query)
267
+ !dir.empty? && !GraphQL::Execution::DirectiveChecks.include?(dir, query)
267
268
  end
268
269
 
269
270
  def on_fragment_with_type(node)
270
271
  object_type = if node.type
271
- @query.warden.get_type(node.type.name)
272
+ @types.type(node.type.name)
272
273
  else
273
274
  @object_types.last
274
275
  end
@@ -55,10 +55,10 @@ module GraphQL
55
55
  .tap { _1.select!(&:analyze?) }
56
56
 
57
57
  analyzers_to_run = query_analyzers + multiplex_analyzers
58
- if analyzers_to_run.any?
58
+ if !analyzers_to_run.empty?
59
59
 
60
60
  analyzers_to_run.select!(&:visit?)
61
- if analyzers_to_run.any?
61
+ if !analyzers_to_run.empty?
62
62
  visitor = GraphQL::Analysis::Visitor.new(
63
63
  query: query,
64
64
  analyzers: analyzers_to_run
@@ -69,7 +69,7 @@ module GraphQL
69
69
  visitor.visit
70
70
  end
71
71
 
72
- if visitor.rescued_errors.any?
72
+ if !visitor.rescued_errors.empty?
73
73
  return visitor.rescued_errors
74
74
  end
75
75
  end
@@ -81,7 +81,7 @@ module GraphQL
81
81
  end
82
82
  rescue Timeout::Error
83
83
  [GraphQL::AnalysisError.new("Timeout on validation of query")]
84
- rescue GraphQL::UnauthorizedError
84
+ rescue GraphQL::UnauthorizedError, GraphQL::ExecutionError
85
85
  # This error was raised during analysis and will be returned the client before execution
86
86
  []
87
87
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ # @see GraphQL::Railtie for automatic Rails integration
5
+ module Autoload
6
+ # Register a constant named `const_name` to be loaded from `path`.
7
+ # This is like `Kernel#autoload` but it tracks the constants so they can be eager-loaded with {#eager_load!}
8
+ # @param const_name [Symbol]
9
+ # @param path [String]
10
+ # @return [void]
11
+ def autoload(const_name, path)
12
+ @_eagerloaded_constants ||= []
13
+ @_eagerloaded_constants << const_name
14
+
15
+ super const_name, path
16
+ end
17
+
18
+ # Call this to load this constant's `autoload` dependents and continue calling recursively
19
+ # @return [void]
20
+ def eager_load!
21
+ @_eager_loading = true
22
+ if @_eagerloaded_constants
23
+ @_eagerloaded_constants.each { |const_name| const_get(const_name) }
24
+ @_eagerloaded_constants = nil
25
+ end
26
+ nil
27
+ ensure
28
+ @_eager_loading = false
29
+ end
30
+
31
+ private
32
+
33
+ # @return [Boolean] `true` if GraphQL-Ruby is currently eager-loading its constants
34
+ def eager_loading?
35
+ @_eager_loading ||= false
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ # This module exposes Fiber-level runtime information.
5
+ #
6
+ # It won't work across unrelated fibers, although it will work in child Fibers.
7
+ #
8
+ # @example Setting Up ActiveRecord::QueryLogs
9
+ #
10
+ # config.active_record.query_log_tags = [
11
+ # :namespaced_controller,
12
+ # :action,
13
+ # :job,
14
+ # # ...
15
+ # {
16
+ # # GraphQL runtime info:
17
+ # current_graphql_operation: -> { GraphQL::Current.operation_name },
18
+ # current_graphql_field: -> { GraphQL::Current.field&.path },
19
+ # current_dataloader_source: -> { GraphQL::Current.dataloader_source_class },
20
+ # # ...
21
+ # },
22
+ # ]
23
+ #
24
+ module Current
25
+ # @return [String, nil] Comma-joined operation names for the currently-running {Multiplex}. `nil` if all operations are anonymous.
26
+ def self.operation_name
27
+ if (m = Fiber[:__graphql_current_multiplex])
28
+ m.context[:__graphql_current_operation_name] ||= begin
29
+ names = m.queries.map { |q| q.selected_operation_name }
30
+ if names.all?(&:nil?)
31
+ nil
32
+ else
33
+ names.join(",")
34
+ end
35
+ end
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ # @see GraphQL::Field#path for a string identifying this field
42
+ # @return [GraphQL::Field, nil] The currently-running field, if there is one.
43
+ def self.field
44
+ Fiber[:__graphql_runtime_info]&.values&.first&.current_field
45
+ end
46
+
47
+ # @return [Class, nil] The currently-running {Dataloader::Source} class, if there is one.
48
+ def self.dataloader_source_class
49
+ Fiber[:__graphql_current_dataloader_source]&.class
50
+ end
51
+ end
52
+ end
@@ -3,7 +3,7 @@ module GraphQL
3
3
  class Dataloader
4
4
  class AsyncDataloader < Dataloader
5
5
  def yield
6
- if (condition = Thread.current[:graphql_dataloader_next_tick])
6
+ if (condition = Fiber[:graphql_dataloader_next_tick])
7
7
  condition.wait
8
8
  else
9
9
  Fiber.yield
@@ -12,6 +12,7 @@ module GraphQL
12
12
  end
13
13
 
14
14
  def run
15
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
15
16
  job_fibers = []
16
17
  next_job_fibers = []
17
18
  source_tasks = []
@@ -19,11 +20,11 @@ module GraphQL
19
20
  first_pass = true
20
21
  sources_condition = Async::Condition.new
21
22
  manager = spawn_fiber do
22
- while first_pass || job_fibers.any?
23
+ while first_pass || !job_fibers.empty?
23
24
  first_pass = false
24
25
  fiber_vars = get_fiber_variables
25
26
 
26
- while (f = (job_fibers.shift || spawn_job_fiber))
27
+ while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber)))
27
28
  if f.alive?
28
29
  finished = run_fiber(f)
29
30
  if !finished
@@ -36,8 +37,8 @@ module GraphQL
36
37
 
37
38
  Sync do |root_task|
38
39
  set_fiber_variables(fiber_vars)
39
- while source_tasks.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
40
- while (task = source_tasks.shift || spawn_source_task(root_task, sources_condition))
40
+ while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
41
+ while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition))))
41
42
  if task.alive?
42
43
  root_task.yield # give the source task a chance to run
43
44
  next_source_tasks << task
@@ -77,7 +78,7 @@ module GraphQL
77
78
  fiber_vars = get_fiber_variables
78
79
  parent_task.async do
79
80
  set_fiber_variables(fiber_vars)
80
- Thread.current[:graphql_dataloader_next_tick] = condition
81
+ Fiber[:graphql_dataloader_next_tick] = condition
81
82
  pending_sources.each(&:run_pending_keys)
82
83
  cleanup_fiber
83
84
  end
@@ -73,7 +73,7 @@ module GraphQL
73
73
  end
74
74
  }
75
75
 
76
- if pending_keys.any?
76
+ if !pending_keys.empty?
77
77
  sync(pending_keys)
78
78
  end
79
79
 
@@ -98,7 +98,7 @@ module GraphQL
98
98
  while pending_result_keys.any? { |key| !@results.key?(key) }
99
99
  iterations += 1
100
100
  if iterations > MAX_ITERATIONS
101
- raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency."
101
+ raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency#{@dataloader.fiber_limit ? " or `fiber_limit: #{@dataloader.fiber_limit}` is set too low" : ""}."
102
102
  end
103
103
  @dataloader.yield
104
104
  end
@@ -186,8 +186,11 @@ This key should have been loaded already. This is a bug in GraphQL::Dataloader,
186
186
  ERR
187
187
  end
188
188
  result = @results[key]
189
-
190
- raise result if result.class <= StandardError
189
+ if result.is_a?(StandardError)
190
+ # Dup it because the rescuer may modify it.
191
+ # (This happens for GraphQL::ExecutionErrors, at least)
192
+ raise result.dup
193
+ end
191
194
 
192
195
  result
193
196
  end
@@ -24,18 +24,23 @@ module GraphQL
24
24
  #
25
25
  class Dataloader
26
26
  class << self
27
- attr_accessor :default_nonblocking
27
+ attr_accessor :default_nonblocking, :default_fiber_limit
28
28
  end
29
29
 
30
- NonblockingDataloader = Class.new(self) { self.default_nonblocking = true }
31
-
32
- def self.use(schema, nonblocking: nil)
33
- schema.dataloader_class = if nonblocking
30
+ def self.use(schema, nonblocking: nil, fiber_limit: nil)
31
+ dataloader_class = if nonblocking
34
32
  warn("`nonblocking: true` is deprecated from `GraphQL::Dataloader`, please use `GraphQL::Dataloader::AsyncDataloader` instead. Docs: https://graphql-ruby.org/dataloader/async_dataloader.")
35
- NonblockingDataloader
33
+ Class.new(self) { self.default_nonblocking = true }
36
34
  else
37
35
  self
38
36
  end
37
+
38
+ if fiber_limit
39
+ dataloader_class = Class.new(dataloader_class)
40
+ dataloader_class.default_fiber_limit = fiber_limit
41
+ end
42
+
43
+ schema.dataloader_class = dataloader_class
39
44
  end
40
45
 
41
46
  # Call the block with a Dataloader instance,
@@ -50,14 +55,18 @@ module GraphQL
50
55
  result
51
56
  end
52
57
 
53
- def initialize(nonblocking: self.class.default_nonblocking)
58
+ def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit)
54
59
  @source_cache = Hash.new { |h, k| h[k] = {} }
55
60
  @pending_jobs = []
56
61
  if !nonblocking.nil?
57
62
  @nonblocking = nonblocking
58
63
  end
64
+ @fiber_limit = fiber_limit
59
65
  end
60
66
 
67
+ # @return [Integer, nil]
68
+ attr_reader :fiber_limit
69
+
61
70
  def nonblocking?
62
71
  @nonblocking
63
72
  end
@@ -69,10 +78,7 @@ module GraphQL
69
78
  def get_fiber_variables
70
79
  fiber_vars = {}
71
80
  Thread.current.keys.each do |fiber_var_key|
72
- # This variable should be fresh in each new fiber
73
- if fiber_var_key != :__graphql_runtime_info
74
- fiber_vars[fiber_var_key] = Thread.current[fiber_var_key]
75
- end
81
+ fiber_vars[fiber_var_key] = Thread.current[fiber_var_key]
76
82
  end
77
83
  fiber_vars
78
84
  end
@@ -178,16 +184,17 @@ module GraphQL
178
184
  end
179
185
 
180
186
  def run
187
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
181
188
  job_fibers = []
182
189
  next_job_fibers = []
183
190
  source_fibers = []
184
191
  next_source_fibers = []
185
192
  first_pass = true
186
193
  manager = spawn_fiber do
187
- while first_pass || job_fibers.any?
194
+ while first_pass || !job_fibers.empty?
188
195
  first_pass = false
189
196
 
190
- while (f = (job_fibers.shift || spawn_job_fiber))
197
+ while (f = (job_fibers.shift || (((next_job_fibers.size + job_fibers.size) < jobs_fiber_limit) && spawn_job_fiber)))
191
198
  if f.alive?
192
199
  finished = run_fiber(f)
193
200
  if !finished
@@ -197,8 +204,8 @@ module GraphQL
197
204
  end
198
205
  join_queues(job_fibers, next_job_fibers)
199
206
 
200
- while source_fibers.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
201
- while (f = source_fibers.shift || spawn_source_fiber)
207
+ while (!source_fibers.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) })
208
+ while (f = source_fibers.shift || (((job_fibers.size + source_fibers.size + next_source_fibers.size + next_job_fibers.size) < total_fiber_limit) && spawn_source_fiber))
202
209
  if f.alive?
203
210
  finished = run_fiber(f)
204
211
  if !finished
@@ -217,10 +224,10 @@ module GraphQL
217
224
  raise "Invariant: Manager fiber didn't terminate properly."
218
225
  end
219
226
 
220
- if job_fibers.any?
227
+ if !job_fibers.empty?
221
228
  raise "Invariant: job fibers should have exited but #{job_fibers.size} remained"
222
229
  end
223
- if source_fibers.any?
230
+ if !source_fibers.empty?
224
231
  raise "Invariant: source fibers should have exited but #{source_fibers.size} remained"
225
232
  end
226
233
  rescue UncaughtThrowError => e
@@ -242,6 +249,17 @@ module GraphQL
242
249
 
243
250
  private
244
251
 
252
+ def calculate_fiber_limit
253
+ total_fiber_limit = @fiber_limit || Float::INFINITY
254
+ if total_fiber_limit < 4
255
+ raise ArgumentError, "Dataloader fiber limit is too low (#{total_fiber_limit}), it must be at least 4"
256
+ end
257
+ total_fiber_limit -= 1 # deduct one fiber for `manager`
258
+ # Deduct at least one fiber for sources
259
+ jobs_fiber_limit = total_fiber_limit - 2
260
+ return jobs_fiber_limit, total_fiber_limit
261
+ end
262
+
245
263
  def join_queues(prev_queue, new_queue)
246
264
  @nonblocking && Fiber.scheduler.run
247
265
  prev_queue.concat(new_queue)
@@ -249,7 +267,7 @@ module GraphQL
249
267
  end
250
268
 
251
269
  def spawn_job_fiber
252
- if @pending_jobs.any?
270
+ if !@pending_jobs.empty?
253
271
  spawn_fiber do
254
272
  while job = @pending_jobs.shift
255
273
  job.call
@@ -271,7 +289,10 @@ module GraphQL
271
289
 
272
290
  if pending_sources
273
291
  spawn_fiber do
274
- pending_sources.each(&:run_pending_keys)
292
+ pending_sources.each do |source|
293
+ Fiber[:__graphql_current_dataloader_source] = source
294
+ source.run_pending_keys
295
+ end
275
296
  end
276
297
  end
277
298
  end
@@ -8,22 +8,17 @@ module GraphQL
8
8
  @query = query
9
9
  @dataloader = query.context.dataloader
10
10
  @storage = Hash.new do |h, argument_owner|
11
- args_by_parent = if argument_owner.arguments_statically_coercible?
11
+ h[argument_owner] = if argument_owner.arguments_statically_coercible?
12
12
  shared_values_cache = {}
13
13
  Hash.new do |h2, ignored_parent_object|
14
14
  h2[ignored_parent_object] = shared_values_cache
15
- end
15
+ end.compare_by_identity
16
16
  else
17
17
  Hash.new do |h2, parent_object|
18
- args_by_node = {}
19
- args_by_node.compare_by_identity
20
- h2[parent_object] = args_by_node
21
- end
18
+ h2[parent_object] = {}.compare_by_identity
19
+ end.compare_by_identity
22
20
  end
23
- args_by_parent.compare_by_identity
24
- h[argument_owner] = args_by_parent
25
- end
26
- @storage.compare_by_identity
21
+ end.compare_by_identity
27
22
  end
28
23
 
29
24
  def fetch(ast_node, argument_owner, parent_object)
@@ -12,13 +12,17 @@ module GraphQL
12
12
  end
13
13
 
14
14
  def self.resolve_each_depth(lazies_at_depth, dataloader)
15
- depths = lazies_at_depth.keys
16
- depths.sort!
17
- next_depth = depths.first
18
- if next_depth
19
- lazies = lazies_at_depth[next_depth]
20
- lazies_at_depth.delete(next_depth)
21
- if lazies.any?
15
+ smallest_depth = nil
16
+ lazies_at_depth.each_key do |depth_key|
17
+ smallest_depth ||= depth_key
18
+ if depth_key < smallest_depth
19
+ smallest_depth = depth_key
20
+ end
21
+ end
22
+
23
+ if smallest_depth
24
+ lazies = lazies_at_depth.delete(smallest_depth)
25
+ if !lazies.empty?
22
26
  dataloader.append_job {
23
27
  lazies.each(&:value) # resolve these Lazy instances
24
28
  }
@@ -51,7 +55,7 @@ module GraphQL
51
55
  # these approaches.
52
56
  dataloader.run
53
57
  next_results = []
54
- while results.any?
58
+ while !results.empty?
55
59
  result_value = results.shift
56
60
  if result_value.is_a?(Runtime::GraphQLResultHash) || result_value.is_a?(Hash)
57
61
  results.concat(result_value.values)
@@ -77,7 +81,7 @@ module GraphQL
77
81
  end
78
82
  end
79
83
 
80
- if next_results.any?
84
+ if !next_results.empty?
81
85
  # Any pending data loader jobs may populate the
82
86
  # resutl arrays or result hashes accumulated in
83
87
  # `next_results``. Run those **to completion**