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
@@ -2,23 +2,64 @@
2
2
 
3
3
  module GraphQL
4
4
  class Dataloader
5
- # The default implementation of dataloading -- all no-ops.
5
+ # GraphQL-Ruby uses this when Dataloader isn't enabled.
6
6
  #
7
- # The Dataloader interface isn't public, but it enables
8
- # simple internal code while adding the option to add Dataloader.
7
+ # It runs execution code inline and gathers lazy objects (eg. Promises)
8
+ # and resolves them during {#run}.
9
9
  class NullDataloader < Dataloader
10
- # These are all no-ops because code was
11
- # executed sychronously.
12
- def run; end
13
- def run_isolated; yield; end
14
- def yield
10
+ def initialize(*)
11
+ @lazies_at_depth = Hash.new { |h,k| h[k] = [] }
12
+ end
13
+
14
+ def freeze
15
+ @lazies_at_depth.default_proc = nil
16
+ @lazies_at_depth.freeze
17
+ super
18
+ end
19
+
20
+ def run(trace_query_lazy: nil)
21
+ with_trace_query_lazy(trace_query_lazy) do
22
+ while !@lazies_at_depth.empty?
23
+ smallest_depth = nil
24
+ @lazies_at_depth.each_key do |depth_key|
25
+ smallest_depth ||= depth_key
26
+ if depth_key < smallest_depth
27
+ smallest_depth = depth_key
28
+ end
29
+ end
30
+
31
+ if smallest_depth
32
+ lazies = @lazies_at_depth.delete(smallest_depth)
33
+ lazies.each(&:value) # resolve these Lazy instances
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def run_isolated
40
+ new_dl = self.class.new
41
+ res = nil
42
+ new_dl.append_job {
43
+ res = yield
44
+ }
45
+ new_dl.run
46
+ res
47
+ end
48
+
49
+ def clear_cache; end
50
+
51
+ def yield(_source)
15
52
  raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
16
53
  end
17
54
 
18
- def append_job
19
- yield
55
+ def append_job(callable = nil)
56
+ callable ? callable.call : yield
20
57
  nil
21
58
  end
59
+
60
+ def with(*)
61
+ raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
62
+ end
22
63
  end
23
64
  end
24
65
  end
@@ -21,7 +21,7 @@ module GraphQL
21
21
  def request(value)
22
22
  res_key = result_key_for(value)
23
23
  if !@results.key?(res_key)
24
- @pending[res_key] ||= value
24
+ @pending[res_key] ||= normalize_fetch_key(value)
25
25
  end
26
26
  Dataloader::Request.new(self, value)
27
27
  end
@@ -35,12 +35,24 @@ module GraphQL
35
35
  value
36
36
  end
37
37
 
38
+ # Implement this method if varying values given to {load} (etc) should be consolidated
39
+ # or normalized before being handed off to your {fetch} implementation.
40
+ #
41
+ # This is different than {result_key_for} because _that_ method handles unification inside Dataloader's cache,
42
+ # but this method changes the value passed into {fetch}.
43
+ #
44
+ # @param value [Object] The value passed to {load}, {load_all}, {request}, or {request_all}
45
+ # @return [Object] The value given to {fetch}
46
+ def normalize_fetch_key(value)
47
+ value
48
+ end
49
+
38
50
  # @return [Dataloader::Request] a pending request for a values from `keys`. Call `.load` on that object to wait for the results.
39
51
  def request_all(values)
40
52
  values.each do |v|
41
53
  res_key = result_key_for(v)
42
54
  if !@results.key?(res_key)
43
- @pending[res_key] ||= v
55
+ @pending[res_key] ||= normalize_fetch_key(v)
44
56
  end
45
57
  end
46
58
  Dataloader::RequestAll.new(self, values)
@@ -53,7 +65,7 @@ module GraphQL
53
65
  if @results.key?(result_key)
54
66
  result_for(result_key)
55
67
  else
56
- @pending[result_key] ||= value
68
+ @pending[result_key] ||= normalize_fetch_key(value)
57
69
  sync([result_key])
58
70
  result_for(result_key)
59
71
  end
@@ -68,12 +80,12 @@ module GraphQL
68
80
  k = result_key_for(v)
69
81
  result_keys << k
70
82
  if !@results.key?(k)
71
- @pending[k] ||= v
83
+ @pending[k] ||= normalize_fetch_key(v)
72
84
  pending_keys << k
73
85
  end
74
86
  }
75
87
 
76
- if pending_keys.any?
88
+ if !pending_keys.empty?
77
89
  sync(pending_keys)
78
90
  end
79
91
 
@@ -93,14 +105,14 @@ module GraphQL
93
105
  # Then run the batch and update the cache.
94
106
  # @return [void]
95
107
  def sync(pending_result_keys)
96
- @dataloader.yield
108
+ @dataloader.yield(self)
97
109
  iterations = 0
98
110
  while pending_result_keys.any? { |key| !@results.key?(key) }
99
111
  iterations += 1
100
112
  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."
113
+ 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
114
  end
103
- @dataloader.yield
115
+ @dataloader.yield(self)
104
116
  end
105
117
  nil
106
118
  end
@@ -186,7 +198,6 @@ This key should have been loaded already. This is a bug in GraphQL::Dataloader,
186
198
  ERR
187
199
  end
188
200
  result = @results[key]
189
-
190
201
  if result.is_a?(StandardError)
191
202
  # Dup it because the rescuer may modify it.
192
203
  # (This happens for GraphQL::ExecutionErrors, at least)
@@ -4,6 +4,8 @@ require "graphql/dataloader/null_dataloader"
4
4
  require "graphql/dataloader/request"
5
5
  require "graphql/dataloader/request_all"
6
6
  require "graphql/dataloader/source"
7
+ require "graphql/dataloader/active_record_association_source"
8
+ require "graphql/dataloader/active_record_source"
7
9
 
8
10
  module GraphQL
9
11
  # This plugin supports Fiber-based concurrency, along with {GraphQL::Dataloader::Source}.
@@ -24,18 +26,23 @@ module GraphQL
24
26
  #
25
27
  class Dataloader
26
28
  class << self
27
- attr_accessor :default_nonblocking
29
+ attr_accessor :default_nonblocking, :default_fiber_limit
28
30
  end
29
31
 
30
- NonblockingDataloader = Class.new(self) { self.default_nonblocking = true }
31
-
32
- def self.use(schema, nonblocking: nil)
33
- schema.dataloader_class = if nonblocking
32
+ def self.use(schema, nonblocking: nil, fiber_limit: nil)
33
+ dataloader_class = if nonblocking
34
34
  warn("`nonblocking: true` is deprecated from `GraphQL::Dataloader`, please use `GraphQL::Dataloader::AsyncDataloader` instead. Docs: https://graphql-ruby.org/dataloader/async_dataloader.")
35
- NonblockingDataloader
35
+ Class.new(self) { self.default_nonblocking = true }
36
36
  else
37
37
  self
38
38
  end
39
+
40
+ if fiber_limit
41
+ dataloader_class = Class.new(dataloader_class)
42
+ dataloader_class.default_fiber_limit = fiber_limit
43
+ end
44
+
45
+ schema.dataloader_class = dataloader_class
39
46
  end
40
47
 
41
48
  # Call the block with a Dataloader instance,
@@ -50,14 +57,19 @@ module GraphQL
50
57
  result
51
58
  end
52
59
 
53
- def initialize(nonblocking: self.class.default_nonblocking)
60
+ def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit)
54
61
  @source_cache = Hash.new { |h, k| h[k] = {} }
55
62
  @pending_jobs = []
56
63
  if !nonblocking.nil?
57
64
  @nonblocking = nonblocking
58
65
  end
66
+ @fiber_limit = fiber_limit
67
+ @lazies_at_depth = Hash.new { |h, k| h[k] = [] }
59
68
  end
60
69
 
70
+ # @return [Integer, nil]
71
+ attr_reader :fiber_limit
72
+
61
73
  def nonblocking?
62
74
  @nonblocking
63
75
  end
@@ -69,10 +81,7 @@ module GraphQL
69
81
  def get_fiber_variables
70
82
  fiber_vars = {}
71
83
  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
84
+ fiber_vars[fiber_var_key] = Thread.current[fiber_var_key]
76
85
  end
77
86
  fiber_vars
78
87
  end
@@ -88,6 +97,11 @@ module GraphQL
88
97
  nil
89
98
  end
90
99
 
100
+ # This method is called when Dataloader is finished using a fiber.
101
+ # Use it to perform any cleanup, such as releasing database connections (if required manually)
102
+ def cleanup_fiber
103
+ end
104
+
91
105
  # Get a Source instance from this dataloader, for calling `.load(...)` or `.request(...)` on.
92
106
  #
93
107
  # @param source_class [Class<GraphQL::Dataloader::Source]
@@ -118,16 +132,19 @@ module GraphQL
118
132
  # Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
119
133
  #
120
134
  # @return [void]
121
- def yield
135
+ def yield(source = Fiber[:__graphql_current_dataloader_source])
136
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
137
+ trace&.dataloader_fiber_yield(source)
122
138
  Fiber.yield
139
+ trace&.dataloader_fiber_resume(source)
123
140
  nil
124
141
  end
125
142
 
126
143
  # @api private Nothing to see here
127
- def append_job(&job)
144
+ def append_job(callable = nil, &job)
128
145
  # Given a block, queue it up to be worked through when `#run` is called.
129
- # (If the dataloader is already running, than a Fiber will pick this up later.)
130
- @pending_jobs.push(job)
146
+ # (If the dataloader is already running, then a Fiber will pick this up later.)
147
+ @pending_jobs.push(callable || job)
131
148
  nil
132
149
  end
133
150
 
@@ -144,6 +161,10 @@ module GraphQL
144
161
  def run_isolated
145
162
  prev_queue = @pending_jobs
146
163
  prev_pending_keys = {}
164
+ prev_lazies_at_depth = @lazies_at_depth
165
+ @lazies_at_depth = @lazies_at_depth.dup.clear
166
+ # Clear pending loads but keep already-cached records
167
+ # in case they are useful to the given block.
147
168
  @source_cache.each do |source_class, batched_sources|
148
169
  batched_sources.each do |batch_args, batched_source_instance|
149
170
  if batched_source_instance.pending?
@@ -163,6 +184,7 @@ module GraphQL
163
184
  res
164
185
  ensure
165
186
  @pending_jobs = prev_queue
187
+ @lazies_at_depth = prev_lazies_at_depth
166
188
  prev_pending_keys.each do |source_instance, pending|
167
189
  pending.each do |key, value|
168
190
  if !source_instance.results.key?(key)
@@ -172,38 +194,31 @@ module GraphQL
172
194
  end
173
195
  end
174
196
 
175
- def run
197
+ # @param trace_query_lazy [nil, Execution::Multiplex]
198
+ def run(trace_query_lazy: nil)
199
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
200
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
176
201
  job_fibers = []
177
202
  next_job_fibers = []
178
203
  source_fibers = []
179
204
  next_source_fibers = []
180
205
  first_pass = true
181
206
  manager = spawn_fiber do
182
- while first_pass || job_fibers.any?
207
+ trace&.begin_dataloader(self)
208
+ while first_pass || !job_fibers.empty?
183
209
  first_pass = false
184
210
 
185
- while (f = (job_fibers.shift || spawn_job_fiber))
186
- if f.alive?
187
- finished = run_fiber(f)
188
- if !finished
189
- next_job_fibers << f
190
- end
191
- end
192
- end
193
- join_queues(job_fibers, next_job_fibers)
194
-
195
- while source_fibers.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
196
- while (f = source_fibers.shift || spawn_source_fiber)
197
- if f.alive?
198
- finished = run_fiber(f)
199
- if !finished
200
- next_source_fibers << f
201
- end
202
- end
211
+ run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit)
212
+
213
+ if !@lazies_at_depth.empty?
214
+ with_trace_query_lazy(trace_query_lazy) do
215
+ run_next_pending_lazies(job_fibers, trace)
216
+ run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit)
203
217
  end
204
- join_queues(source_fibers, next_source_fibers)
205
218
  end
206
219
  end
220
+
221
+ trace&.end_dataloader(self)
207
222
  end
208
223
 
209
224
  run_fiber(manager)
@@ -212,12 +227,13 @@ module GraphQL
212
227
  raise "Invariant: Manager fiber didn't terminate properly."
213
228
  end
214
229
 
215
- if job_fibers.any?
230
+ if !job_fibers.empty?
216
231
  raise "Invariant: job fibers should have exited but #{job_fibers.size} remained"
217
232
  end
218
- if source_fibers.any?
233
+ if !source_fibers.empty?
219
234
  raise "Invariant: source fibers should have exited but #{source_fibers.size} remained"
220
235
  end
236
+
221
237
  rescue UncaughtThrowError => e
222
238
  throw e.tag, e.value
223
239
  end
@@ -226,36 +242,121 @@ module GraphQL
226
242
  f.resume
227
243
  end
228
244
 
245
+ # @api private
246
+ def lazy_at_depth(depth, lazy)
247
+ @lazies_at_depth[depth] << lazy
248
+ end
249
+
229
250
  def spawn_fiber
230
251
  fiber_vars = get_fiber_variables
231
252
  Fiber.new(blocking: !@nonblocking) {
232
253
  set_fiber_variables(fiber_vars)
233
254
  yield
234
- # With `.transfer`, you have to explicitly pass back to the parent --
235
- # if the fiber is allowed to terminate normally, control is passed to the main fiber instead.
236
- true
255
+ cleanup_fiber
237
256
  }
238
257
  end
239
258
 
259
+ # Pre-warm the Dataloader cache with ActiveRecord objects which were loaded elsewhere.
260
+ # These will be used by {Dataloader::ActiveRecordSource}, {Dataloader::ActiveRecordAssociationSource} and their helper
261
+ # methods, `dataload_record` and `dataload_association`.
262
+ # @param records [Array<ActiveRecord::Base>] Already-loaded records to warm the cache with
263
+ # @param index_by [Symbol] The attribute to use as the cache key. (Should match `find_by:` when using {ActiveRecordSource})
264
+ # @return [void]
265
+ def merge_records(records, index_by: :id)
266
+ records_by_class = Hash.new { |h, k| h[k] = {} }
267
+ records.each do |r|
268
+ records_by_class[r.class][r.public_send(index_by)] = r
269
+ end
270
+ records_by_class.each do |r_class, records|
271
+ with(ActiveRecordSource, r_class).merge(records)
272
+ end
273
+ end
274
+
240
275
  private
241
276
 
277
+ def run_next_pending_lazies(job_fibers, trace)
278
+ smallest_depth = nil
279
+ @lazies_at_depth.each_key do |depth_key|
280
+ smallest_depth ||= depth_key
281
+ if depth_key < smallest_depth
282
+ smallest_depth = depth_key
283
+ end
284
+ end
285
+
286
+ if smallest_depth
287
+ lazies = @lazies_at_depth.delete(smallest_depth)
288
+ if !lazies.empty?
289
+ lazies.each_with_index do |l, idx|
290
+ append_job { l.value }
291
+ end
292
+ job_fibers.unshift(spawn_job_fiber(trace))
293
+ end
294
+ end
295
+ end
296
+
297
+ def run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit)
298
+ while (f = (job_fibers.shift || (((next_job_fibers.size + job_fibers.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
299
+ if f.alive?
300
+ finished = run_fiber(f)
301
+ if !finished
302
+ next_job_fibers << f
303
+ end
304
+ end
305
+ end
306
+ join_queues(job_fibers, next_job_fibers)
307
+
308
+ while (!source_fibers.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) })
309
+ 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(trace)))
310
+ if f.alive?
311
+ finished = run_fiber(f)
312
+ if !finished
313
+ next_source_fibers << f
314
+ end
315
+ end
316
+ end
317
+ join_queues(source_fibers, next_source_fibers)
318
+ end
319
+ end
320
+
321
+ def with_trace_query_lazy(multiplex_or_nil, &block)
322
+ if (multiplex = multiplex_or_nil)
323
+ query = multiplex.queries.length == 1 ? multiplex.queries[0] : nil
324
+ multiplex.current_trace.execute_query_lazy(query: query, multiplex: multiplex, &block)
325
+ else
326
+ yield
327
+ end
328
+ end
329
+
330
+ def calculate_fiber_limit
331
+ total_fiber_limit = @fiber_limit || Float::INFINITY
332
+ if total_fiber_limit < 4
333
+ raise ArgumentError, "Dataloader fiber limit is too low (#{total_fiber_limit}), it must be at least 4"
334
+ end
335
+ total_fiber_limit -= 1 # deduct one fiber for `manager`
336
+ # Deduct at least one fiber for sources
337
+ jobs_fiber_limit = total_fiber_limit - 2
338
+ return jobs_fiber_limit, total_fiber_limit
339
+ end
340
+
242
341
  def join_queues(prev_queue, new_queue)
243
342
  @nonblocking && Fiber.scheduler.run
244
343
  prev_queue.concat(new_queue)
245
344
  new_queue.clear
246
345
  end
247
346
 
248
- def spawn_job_fiber
249
- if @pending_jobs.any?
347
+ def spawn_job_fiber(trace)
348
+ if !@pending_jobs.empty?
250
349
  spawn_fiber do
350
+ trace&.dataloader_spawn_execution_fiber(@pending_jobs)
251
351
  while job = @pending_jobs.shift
252
352
  job.call
253
353
  end
354
+ trace&.dataloader_fiber_exit
254
355
  end
255
356
  end
256
357
  end
257
358
 
258
- def spawn_source_fiber
359
+ def spawn_source_fiber(trace)
259
360
  pending_sources = nil
260
361
  @source_cache.each_value do |source_by_batch_params|
261
362
  source_by_batch_params.each_value do |source|
@@ -268,7 +369,14 @@ module GraphQL
268
369
 
269
370
  if pending_sources
270
371
  spawn_fiber do
271
- pending_sources.each(&:run_pending_keys)
372
+ trace&.dataloader_spawn_source_fiber(pending_sources)
373
+ pending_sources.each do |source|
374
+ Fiber[:__graphql_current_dataloader_source] = source
375
+ trace&.begin_dataloader_source(source)
376
+ source.run_pending_keys
377
+ trace&.end_dataloader_source(source)
378
+ end
379
+ trace&.dataloader_fiber_exit
272
380
  end
273
381
  end
274
382
  end
@@ -10,7 +10,7 @@ module GraphQL
10
10
 
11
11
  def initialize(value)
12
12
  @date_value = value
13
- super("Date cannot be parsed: #{value}. \nDate must be be able to be parsed as a Ruby Date object.")
13
+ super("Date cannot be parsed: #{value}. \nDate must be able to be parsed as a Ruby Date object.")
14
14
  end
15
15
  end
16
16
  end
data/lib/graphql/dig.rb CHANGED
@@ -5,7 +5,8 @@ module GraphQL
5
5
  # so we can use some of the magic in Schema::InputObject and Interpreter::Arguments
6
6
  # to handle stringified/symbolized keys.
7
7
  #
8
- # @param args [Array<[String, Symbol>] Retrieves the value object corresponding to the each key objects repeatedly
8
+ # @param own_key [String, Symbol] A key to retrieve
9
+ # @param rest_keys [Array<[String, Symbol>] Retrieves the value object corresponding to the each key objects repeatedly
9
10
  # @return [Object]
10
11
  def dig(own_key, *rest_keys)
11
12
  val = self[own_key]
@@ -6,15 +6,19 @@ module GraphQL
6
6
  # A container for metadata regarding arguments present in a GraphQL query.
7
7
  # @see Interpreter::Arguments#argument_values for a hash of these objects.
8
8
  class ArgumentValue
9
- def initialize(definition:, value:, default_used:)
9
+ def initialize(definition:, value:, original_value:, default_used:)
10
10
  @definition = definition
11
11
  @value = value
12
+ @original_value = original_value
12
13
  @default_used = default_used
13
14
  end
14
15
 
15
16
  # @return [Object] The Ruby-ready value for this Argument
16
17
  attr_reader :value
17
18
 
19
+ # @return [Object] The value of this argument _before_ `prepare` is applied.
20
+ attr_reader :original_value
21
+
18
22
  # @return [GraphQL::Schema::Argument] The definition instance for this argument
19
23
  attr_reader :definition
20
24
 
@@ -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)
@@ -6,22 +6,31 @@ module GraphQL
6
6
  module Resolve
7
7
  # Continue field results in `results` until there's nothing else to continue.
8
8
  # @return [void]
9
+ # @deprecated Call `dataloader.run` instead
9
10
  def self.resolve_all(results, dataloader)
11
+ warn "#{self}.#{__method__} is deprecated; Use `dataloader.run` instead.#{caller(1, 5).map { |l| "\n #{l}"}.join}"
10
12
  dataloader.append_job { resolve(results, dataloader) }
11
13
  nil
12
14
  end
13
15
 
16
+ # @deprecated Call `dataloader.run` instead
14
17
  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?
22
- dataloader.append_job {
23
- lazies.each(&:value) # resolve these Lazy instances
24
- }
18
+ warn "#{self}.#{__method__} is deprecated; Use `dataloader.run` instead.#{caller(1, 5).map { |l| "\n #{l}"}.join}"
19
+
20
+ smallest_depth = nil
21
+ lazies_at_depth.each_key do |depth_key|
22
+ smallest_depth ||= depth_key
23
+ if depth_key < smallest_depth
24
+ smallest_depth = depth_key
25
+ end
26
+ end
27
+
28
+ if smallest_depth
29
+ lazies = lazies_at_depth.delete(smallest_depth)
30
+ if !lazies.empty?
31
+ lazies.each do |l|
32
+ dataloader.append_job { l.value }
33
+ end
25
34
  # Run lazies _and_ dataloader, see if more are enqueued
26
35
  dataloader.run
27
36
  resolve_each_depth(lazies_at_depth, dataloader)
@@ -30,20 +39,9 @@ module GraphQL
30
39
  nil
31
40
  end
32
41
 
33
- # After getting `results` back from an interpreter evaluation,
34
- # continue it until you get a response-ready Ruby value.
35
- #
36
- # `results` is one level of _depth_ of a query or multiplex.
37
- #
38
- # Resolve all lazy values in that depth before moving on
39
- # to the next level.
40
- #
41
- # It's assumed that the lazies will
42
- # return {Lazy} instances if there's more work to be done,
43
- # or return {Hash}/{Array} if the query should be continued.
44
- #
45
- # @return [void]
42
+ # @deprecated Call `dataloader.run` instead
46
43
  def self.resolve(results, dataloader)
44
+ warn "#{self}.#{__method__} is deprecated; Use `dataloader.run` instead.#{caller(1, 5).map { |l| "\n #{l}"}.join}"
47
45
  # There might be pending jobs here that _will_ write lazies
48
46
  # into the result hash. We should run them out, so we
49
47
  # can be sure that all lazies will be present in the result hashes.
@@ -51,7 +49,7 @@ module GraphQL
51
49
  # these approaches.
52
50
  dataloader.run
53
51
  next_results = []
54
- while results.any?
52
+ while !results.empty?
55
53
  result_value = results.shift
56
54
  if result_value.is_a?(Runtime::GraphQLResultHash) || result_value.is_a?(Hash)
57
55
  results.concat(result_value.values)
@@ -77,7 +75,7 @@ module GraphQL
77
75
  end
78
76
  end
79
77
 
80
- if next_results.any?
78
+ if !next_results.empty?
81
79
  # Any pending data loader jobs may populate the
82
80
  # resutl arrays or result hashes accumulated in
83
81
  # `next_results``. Run those **to completion**