graphql 1.12.16 → 1.13.2

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +3 -1
  3. data/lib/generators/graphql/install_generator.rb +9 -2
  4. data/lib/generators/graphql/mutation_generator.rb +1 -1
  5. data/lib/generators/graphql/object_generator.rb +2 -1
  6. data/lib/generators/graphql/relay.rb +19 -11
  7. data/lib/generators/graphql/templates/schema.erb +14 -2
  8. data/lib/generators/graphql/type_generator.rb +0 -1
  9. data/lib/graphql/analysis/ast/field_usage.rb +3 -3
  10. data/lib/graphql/analysis/ast/query_complexity.rb +10 -14
  11. data/lib/graphql/analysis/ast/visitor.rb +4 -4
  12. data/lib/graphql/backtrace/table.rb +1 -1
  13. data/lib/graphql/base_type.rb +4 -2
  14. data/lib/graphql/boolean_type.rb +1 -1
  15. data/lib/graphql/dataloader/source.rb +50 -2
  16. data/lib/graphql/dataloader.rb +93 -37
  17. data/lib/graphql/define/instance_definable.rb +1 -1
  18. data/lib/graphql/deprecated_dsl.rb +11 -3
  19. data/lib/graphql/deprecation.rb +1 -5
  20. data/lib/graphql/directive/deprecated_directive.rb +1 -1
  21. data/lib/graphql/directive/include_directive.rb +1 -1
  22. data/lib/graphql/directive/skip_directive.rb +1 -1
  23. data/lib/graphql/directive.rb +0 -4
  24. data/lib/graphql/enum_type.rb +5 -1
  25. data/lib/graphql/execution/errors.rb +1 -0
  26. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  27. data/lib/graphql/execution/interpreter/arguments_cache.rb +2 -2
  28. data/lib/graphql/execution/interpreter/runtime.rb +39 -23
  29. data/lib/graphql/execution/lookahead.rb +2 -2
  30. data/lib/graphql/execution/multiplex.rb +4 -1
  31. data/lib/graphql/float_type.rb +1 -1
  32. data/lib/graphql/id_type.rb +1 -1
  33. data/lib/graphql/int_type.rb +1 -1
  34. data/lib/graphql/integer_encoding_error.rb +18 -2
  35. data/lib/graphql/introspection/directive_type.rb +1 -1
  36. data/lib/graphql/introspection/entry_points.rb +2 -2
  37. data/lib/graphql/introspection/enum_value_type.rb +2 -2
  38. data/lib/graphql/introspection/field_type.rb +2 -2
  39. data/lib/graphql/introspection/input_value_type.rb +10 -4
  40. data/lib/graphql/introspection/schema_type.rb +2 -2
  41. data/lib/graphql/introspection/type_type.rb +10 -10
  42. data/lib/graphql/language/block_string.rb +2 -6
  43. data/lib/graphql/language/document_from_schema_definition.rb +4 -2
  44. data/lib/graphql/language/lexer.rb +0 -3
  45. data/lib/graphql/language/lexer.rl +0 -4
  46. data/lib/graphql/language/nodes.rb +12 -2
  47. data/lib/graphql/language/parser.rb +442 -434
  48. data/lib/graphql/language/parser.y +5 -4
  49. data/lib/graphql/language/printer.rb +6 -1
  50. data/lib/graphql/language/sanitized_printer.rb +5 -5
  51. data/lib/graphql/language/token.rb +0 -4
  52. data/lib/graphql/name_validator.rb +0 -4
  53. data/lib/graphql/pagination/connections.rb +35 -16
  54. data/lib/graphql/query/arguments.rb +1 -1
  55. data/lib/graphql/query/arguments_cache.rb +1 -1
  56. data/lib/graphql/query/context.rb +15 -2
  57. data/lib/graphql/query/literal_input.rb +1 -1
  58. data/lib/graphql/query/null_context.rb +12 -7
  59. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -1
  60. data/lib/graphql/query/validation_pipeline.rb +1 -1
  61. data/lib/graphql/query/variables.rb +5 -1
  62. data/lib/graphql/query.rb +4 -0
  63. data/lib/graphql/relay/edges_instrumentation.rb +0 -1
  64. data/lib/graphql/relay/global_id_resolve.rb +1 -1
  65. data/lib/graphql/relay/page_info.rb +1 -1
  66. data/lib/graphql/rubocop/graphql/base_cop.rb +36 -0
  67. data/lib/graphql/rubocop/graphql/default_null_true.rb +43 -0
  68. data/lib/graphql/rubocop/graphql/default_required_true.rb +43 -0
  69. data/lib/graphql/rubocop.rb +4 -0
  70. data/lib/graphql/schema/addition.rb +37 -28
  71. data/lib/graphql/schema/argument.rb +79 -34
  72. data/lib/graphql/schema/build_from_definition.rb +5 -5
  73. data/lib/graphql/schema/directive/feature.rb +1 -1
  74. data/lib/graphql/schema/directive/flagged.rb +2 -2
  75. data/lib/graphql/schema/directive/include.rb +1 -1
  76. data/lib/graphql/schema/directive/skip.rb +1 -1
  77. data/lib/graphql/schema/directive/transform.rb +1 -1
  78. data/lib/graphql/schema/directive.rb +7 -3
  79. data/lib/graphql/schema/enum.rb +60 -10
  80. data/lib/graphql/schema/enum_value.rb +6 -0
  81. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  82. data/lib/graphql/schema/field.rb +140 -42
  83. data/lib/graphql/schema/field_extension.rb +52 -2
  84. data/lib/graphql/schema/find_inherited_value.rb +1 -0
  85. data/lib/graphql/schema/finder.rb +5 -5
  86. data/lib/graphql/schema/input_object.rb +13 -14
  87. data/lib/graphql/schema/interface.rb +11 -20
  88. data/lib/graphql/schema/introspection_system.rb +1 -1
  89. data/lib/graphql/schema/list.rb +3 -1
  90. data/lib/graphql/schema/member/accepts_definition.rb +15 -3
  91. data/lib/graphql/schema/member/build_type.rb +0 -4
  92. data/lib/graphql/schema/member/cached_graphql_definition.rb +29 -2
  93. data/lib/graphql/schema/member/has_arguments.rb +145 -57
  94. data/lib/graphql/schema/member/has_deprecation_reason.rb +1 -1
  95. data/lib/graphql/schema/member/has_fields.rb +76 -18
  96. data/lib/graphql/schema/member/has_interfaces.rb +90 -0
  97. data/lib/graphql/schema/member.rb +1 -0
  98. data/lib/graphql/schema/non_null.rb +3 -1
  99. data/lib/graphql/schema/object.rb +10 -75
  100. data/lib/graphql/schema/printer.rb +1 -1
  101. data/lib/graphql/schema/relay_classic_mutation.rb +37 -3
  102. data/lib/graphql/schema/resolver/has_payload_type.rb +27 -2
  103. data/lib/graphql/schema/resolver.rb +49 -64
  104. data/lib/graphql/schema/scalar.rb +2 -0
  105. data/lib/graphql/schema/subscription.rb +17 -9
  106. data/lib/graphql/schema/traversal.rb +1 -1
  107. data/lib/graphql/schema/type_expression.rb +1 -1
  108. data/lib/graphql/schema/type_membership.rb +18 -4
  109. data/lib/graphql/schema/union.rb +8 -1
  110. data/lib/graphql/schema/validator/allow_blank_validator.rb +29 -0
  111. data/lib/graphql/schema/validator/allow_null_validator.rb +26 -0
  112. data/lib/graphql/schema/validator/exclusion_validator.rb +3 -1
  113. data/lib/graphql/schema/validator/format_validator.rb +4 -5
  114. data/lib/graphql/schema/validator/inclusion_validator.rb +3 -1
  115. data/lib/graphql/schema/validator/length_validator.rb +5 -3
  116. data/lib/graphql/schema/validator/numericality_validator.rb +13 -2
  117. data/lib/graphql/schema/validator.rb +33 -25
  118. data/lib/graphql/schema/warden.rb +116 -52
  119. data/lib/graphql/schema.rb +124 -27
  120. data/lib/graphql/static_validation/base_visitor.rb +8 -5
  121. data/lib/graphql/static_validation/definition_dependencies.rb +0 -1
  122. data/lib/graphql/static_validation/error.rb +3 -1
  123. data/lib/graphql/static_validation/literal_validator.rb +1 -1
  124. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  125. data/lib/graphql/static_validation/rules/fields_will_merge.rb +52 -26
  126. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
  127. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  128. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -1
  129. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -4
  130. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +7 -7
  131. data/lib/graphql/static_validation/validation_context.rb +8 -2
  132. data/lib/graphql/static_validation/validator.rb +15 -12
  133. data/lib/graphql/string_encoding_error.rb +13 -3
  134. data/lib/graphql/string_type.rb +1 -1
  135. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +15 -5
  136. data/lib/graphql/subscriptions/event.rb +66 -13
  137. data/lib/graphql/subscriptions/serialize.rb +1 -1
  138. data/lib/graphql/subscriptions.rb +17 -19
  139. data/lib/graphql/tracing/appsignal_tracing.rb +15 -0
  140. data/lib/graphql/types/int.rb +1 -1
  141. data/lib/graphql/types/relay/connection_behaviors.rb +26 -9
  142. data/lib/graphql/types/relay/default_relay.rb +5 -1
  143. data/lib/graphql/types/relay/edge_behaviors.rb +13 -2
  144. data/lib/graphql/types/relay/has_node_field.rb +1 -1
  145. data/lib/graphql/types/relay/has_nodes_field.rb +1 -1
  146. data/lib/graphql/types/string.rb +1 -1
  147. data/lib/graphql/unauthorized_error.rb +1 -1
  148. data/lib/graphql/version.rb +1 -1
  149. data/lib/graphql.rb +10 -32
  150. data/readme.md +1 -1
  151. metadata +13 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03b30407d3081dad5d25f3a3f9a9e2781cd34db838688816b01c231aaa5a6dc5
4
- data.tar.gz: a06092995f8e3ea0ed2c75485124b0123d41a23d77eedd773e15a1d00fad80ab
3
+ metadata.gz: 381cd8cc5f2508805ccc69172566e28577652372b04e44b236ceaf903db1622f
4
+ data.tar.gz: 56c03d754890c85d6a6c7d5df6a50874f3986a1284f2666a256ea51d90028a82
5
5
  SHA512:
6
- metadata.gz: 3af391b8f7394985a42595af2d7b3735a6666310a4583ffd2a9ddce46cf868a6a8d7f2a9cb28ba6a1b85fb70cce8d7425074df847f8b113420e59599bd0379b5
7
- data.tar.gz: ca7370709ff588ba437a2f5b277482f2a00543b057ee96e164198ea44f81ad1e470fafe4c7eb89a9e1e64c0aeeddcc128983a67458040c855115516c09adcfd0
6
+ metadata.gz: 31f7ab242f8b5efa5f2e77a9c79ab3c4a6b3b9d319dcdc4215d676009f472dc59a1fdf3303a3af86ab557c5b7f53ff2610d30dc36a58eb0937962ebe1ce388f9
7
+ data.tar.gz: 8c6295f3cdae55803aad1bb45abb97ec1f6a5b76d60f2150e449c5e8244619096c9462317358a048b9d5045a0246cfd2354b96d6c4de49968995e86dd6b2aafa
@@ -19,7 +19,9 @@ module Graphql
19
19
  sentinel = /< GraphQL::Schema\s*\n/m
20
20
 
21
21
  in_root do
22
- inject_into_file schema_file_path, " #{type}(Types::#{name})\n", after: sentinel, verbose: false, force: false
22
+ if File.exist?(schema_file_path)
23
+ inject_into_file schema_file_path, " #{type}(Types::#{name})\n", after: sentinel, verbose: false, force: false
24
+ end
23
25
  end
24
26
  end
25
27
 
@@ -122,8 +122,15 @@ module Graphql
122
122
  if options.api?
123
123
  say("Skipped graphiql, as this rails project is API only")
124
124
  say(" You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app")
125
- elsif !options[:skip_graphiql] && !File.read(Rails.root.join("Gemfile")).include?("graphiql-rails")
126
- gem("graphiql-rails", group: :development)
125
+ elsif !options[:skip_graphiql]
126
+ # `gem(...)` uses `gsub_file(...)` under the hood, which is a no-op for `rails destroy...` (when `behavior == :revoke`).
127
+ # So handle that case by calling `gsub_file` with `force: true`.
128
+ if behavior == :invoke && !File.read(Rails.root.join("Gemfile")).include?("graphiql-rails")
129
+ gem("graphiql-rails", group: :development)
130
+ elsif behavior == :revoke
131
+ gemfile_pattern = /\n\s*gem ('|")graphiql-rails('|"), :?group(:| =>) :development/
132
+ gsub_file Rails.root.join("Gemfile"), gemfile_pattern, "", { force: true }
133
+ end
127
134
 
128
135
  # This is a little cheat just to get cleaner shell output:
129
136
  log :route, 'graphiql-rails'
@@ -17,7 +17,7 @@ module Graphql
17
17
 
18
18
  argument :name, type: :string
19
19
 
20
- def initialize(args, *options) #:nodoc:
20
+ def initialize(args, *options) # :nodoc:
21
21
  # Unfreeze name in case it's given as a frozen string
22
22
  args[0] = args[0].dup if args[0].is_a?(String) && args[0].frozen?
23
23
  super
@@ -12,7 +12,8 @@ module Graphql
12
12
  #
13
13
  # Add the Node interface with `--node`.
14
14
  class ObjectGenerator < TypeGeneratorBase
15
- desc "Create a GraphQL::ObjectType with the given name and fields"
15
+ desc "Create a GraphQL::ObjectType with the given name and fields." \
16
+ "If the given type name matches an existing ActiveRecord model, the generated type will automatically include fields for the models database columns."
16
17
  source_root File.expand_path('../templates', __FILE__)
17
18
 
18
19
  argument :custom_fields,
@@ -32,20 +32,28 @@ module Graphql
32
32
 
33
33
  # Return a string UUID for `object`
34
34
  def self.id_from_object(object, type_definition, query_ctx)
35
- # Here's a simple implementation which:
36
- # - joins the type name & object.id
37
- # - encodes it with base64:
38
- # GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id)
35
+ # For example, use Rails' GlobalID library (https://github.com/rails/globalid):
36
+ object_id = object.to_global_id.to_s
37
+ # Remove this redundant prefix to make IDs shorter:
38
+ object_id = object_id.sub("gid://\#{GlobalID.app}/", "")
39
+ encoded_id = Base64.urlsafe_encode64(object_id)
40
+ # Remove the "=" padding
41
+ encoded_id = encoded_id.sub(/=+/, "")
42
+ # Add a type hint
43
+ type_hint = type_definition.graphql_name.first
44
+ "\#{type_hint}_\#{encoded_id}"
39
45
  end
40
46
 
41
47
  # Given a string UUID, find the object
42
- def self.object_from_id(id, query_ctx)
43
- # For example, to decode the UUIDs generated above:
44
- # type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id)
45
- #
46
- # Then, based on `type_name` and `id`
47
- # find an object in your application
48
- # ...
48
+ def self.object_from_id(encoded_id_with_hint, query_ctx)
49
+ # For example, use Rails' GlobalID library (https://github.com/rails/globalid):
50
+ # Split off the type hint
51
+ _type_hint, encoded_id = encoded_id_with_hint.split("_", 2)
52
+ # Decode the ID
53
+ id = Base64.urlsafe_decode64(encoded_id)
54
+ # Rebuild it for Rails then find the object:
55
+ full_global_id = "gid://\#{GlobalID.app}/\#{id}"
56
+ GlobalID::Locator.locate(full_global_id)
49
57
  end
50
58
  RUBY
51
59
  inject_into_file schema_file_path, schema_code, before: /^end\n/m, force: false
@@ -4,11 +4,23 @@ class <%= schema_name %> < GraphQL::Schema
4
4
  <% if options[:batch] %>
5
5
  # GraphQL::Batch setup:
6
6
  use GraphQL::Batch
7
+ <% else %>
8
+ # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
9
+ use GraphQL::Dataloader
7
10
  <% end %>
11
+ # GraphQL-Ruby calls this when something goes wrong while running a query:
12
+ def self.type_error(err, context)
13
+ # if err.is_a?(GraphQL::InvalidNullError)
14
+ # # report to your bug tracker here
15
+ # return nil
16
+ # end
17
+ super
18
+ end
19
+
8
20
  # Union and Interface Resolution
9
21
  def self.resolve_type(abstract_type, obj, ctx)
10
- # TODO: Implement this function
11
- # to return the correct object type for `obj`
22
+ # TODO: Implement this method
23
+ # to return the correct GraphQL object type for `obj`
12
24
  raise(GraphQL::RequiredImplementationMissingError)
13
25
  end
14
26
  end
@@ -13,7 +13,6 @@ module Graphql
13
13
 
14
14
  argument :type_name,
15
15
  type: :string,
16
- required: true,
17
16
  banner: "TypeName",
18
17
  desc: "Name of this object type (expressed as Ruby or GraphQL)"
19
18
 
@@ -36,12 +36,12 @@ module GraphQL
36
36
  end
37
37
 
38
38
  if argument.definition.type.kind.input_object?
39
- extract_deprecated_arguments(argument.value.arguments.argument_values)
40
- elsif argument.definition.type.list?
39
+ extract_deprecated_arguments(argument.value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
40
+ elsif argument.definition.type.list? && !argument.value.nil?
41
41
  argument
42
42
  .value
43
43
  .select { |value| value.respond_to?(:arguments) }
44
- .each { |value| extract_deprecated_arguments(value.arguments.argument_values) }
44
+ .each { |value| extract_deprecated_arguments(value.arguments.argument_values) } # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
45
45
  end
46
46
  end
47
47
  end
@@ -23,18 +23,22 @@ module GraphQL
23
23
 
24
24
  attr_reader :field_definition, :response_path, :query
25
25
 
26
- # @param node [Language::Nodes::Field] The AST node; used for providing argument values when necessary
26
+ # @param parent_type [Class] The owner of `field_definition`
27
27
  # @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration
28
28
  # @param query [GraphQL::Query] Used for `query.possible_types`
29
29
  # @param response_path [Array<String>] The path to the response key for the field
30
- def initialize(node, field_definition, query, response_path)
31
- @node = node
30
+ def initialize(parent_type, field_definition, query, response_path)
31
+ @parent_type = parent_type
32
32
  @field_definition = field_definition
33
33
  @query = query
34
34
  @response_path = response_path
35
35
  @scoped_children = nil
36
+ @nodes = []
36
37
  end
37
38
 
39
+ # @return [Array<GraphQL::Language::Nodes::Field>]
40
+ attr_reader :nodes
41
+
38
42
  # Returns true if this field has no selections, ie, it's a scalar.
39
43
  # We need a quick way to check whether we should continue traversing.
40
44
  def terminal?
@@ -50,16 +54,7 @@ module GraphQL
50
54
  end
51
55
 
52
56
  def own_complexity(child_complexity)
53
- defined_complexity = @field_definition.complexity
54
- case defined_complexity
55
- when Proc
56
- arguments = @query.arguments_for(@node, @field_definition)
57
- defined_complexity.call(@query.context, arguments.keyword_arguments, child_complexity)
58
- when Numeric
59
- defined_complexity + child_complexity
60
- else
61
- raise("Invalid complexity: #{defined_complexity.inspect} on #{@field_definition.name}")
62
- end
57
+ @field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
63
58
  end
64
59
  end
65
60
 
@@ -79,7 +74,8 @@ module GraphQL
79
74
  # then the query would have been rejected as invalid.
80
75
  complexities_on_type = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]
81
76
 
82
- complexity = complexities_on_type.last.scoped_children[parent_type][field_key] ||= ScopedTypeComplexity.new(node, visitor.field_definition, visitor.query, visitor.response_path)
77
+ complexity = complexities_on_type.last.scoped_children[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
78
+ complexity.nodes.push(node)
83
79
  # Push it on the stack.
84
80
  complexities_on_type.push(complexity)
85
81
  end
@@ -100,7 +100,7 @@ module GraphQL
100
100
  def on_field(node, parent)
101
101
  @response_path.push(node.alias || node.name)
102
102
  parent_type = @object_types.last
103
- field_definition = @schema.get_field(parent_type, node.name)
103
+ field_definition = @schema.get_field(parent_type, node.name, @query.context)
104
104
  @field_definitions.push(field_definition)
105
105
  if !field_definition.nil?
106
106
  next_object_type = field_definition.type.unwrap
@@ -138,14 +138,14 @@ module GraphQL
138
138
  argument_defn = if (arg = @argument_definitions.last)
139
139
  arg_type = arg.type.unwrap
140
140
  if arg_type.kind.input_object?
141
- arg_type.arguments[node.name]
141
+ arg_type.get_argument(node.name, @query.context)
142
142
  else
143
143
  nil
144
144
  end
145
145
  elsif (directive_defn = @directive_definitions.last)
146
- directive_defn.arguments[node.name]
146
+ directive_defn.get_argument(node.name, @query.context)
147
147
  elsif (field_defn = @field_definitions.last)
148
- field_defn.arguments[node.name]
148
+ field_defn.get_argument(node.name, @query.context)
149
149
  else
150
150
  nil
151
151
  end
@@ -89,7 +89,7 @@ module GraphQL
89
89
  "#{context_entry.ast_node ? context_entry.ast_node.position.join(":") : ""}",
90
90
  "#{context_entry.field.path}#{field_alias ? " as #{field_alias}" : ""}",
91
91
  "#{context_entry.object.object.inspect}",
92
- context_entry.arguments.to_h.inspect,
92
+ context_entry.arguments.to_h.inspect, # rubocop:disable Development/ContextIsPassedCop -- unrelated method
93
93
  Backtrace::InspectResult.inspect_result(value),
94
94
  ]
95
95
  if (parent = context_entry.parent_frame)
@@ -41,7 +41,9 @@ module GraphQL
41
41
  alias :graphql_name :name
42
42
  # Future-compatible alias
43
43
  # @see {GraphQL::SchemaMember}
44
- alias :graphql_definition :itself
44
+ def graphql_definition(silence_deprecation_warning: false)
45
+ itself
46
+ end
45
47
 
46
48
  def type_class
47
49
  metadata[:type_class]
@@ -194,7 +196,7 @@ module GraphQL
194
196
  resolve_related_type(Object.const_get(type_arg))
195
197
  else
196
198
  if type_arg.respond_to?(:graphql_definition)
197
- type_arg.graphql_definition
199
+ type_arg.graphql_definition(silence_deprecation_warning: true)
198
200
  else
199
201
  type_arg
200
202
  end
@@ -1,2 +1,2 @@
1
1
  # frozen_string_literal: true
2
- GraphQL::BOOLEAN_TYPE = GraphQL::Types::Boolean.graphql_definition
2
+ GraphQL::BOOLEAN_TYPE = GraphQL::Types::Boolean.graphql_definition(silence_deprecation_warning: true)
@@ -6,7 +6,11 @@ module GraphQL
6
6
  # Called by {Dataloader} to prepare the {Source}'s internal state
7
7
  # @api private
8
8
  def setup(dataloader)
9
+ # These keys have been requested but haven't been fetched yet
9
10
  @pending_keys = []
11
+ # These keys have been passed to `fetch` but haven't been finished yet
12
+ @fetching_keys = []
13
+ # { key => result }
10
14
  @results = {}
11
15
  @dataloader = dataloader
12
16
  end
@@ -64,31 +68,68 @@ module GraphQL
64
68
  # Then run the batch and update the cache.
65
69
  # @return [void]
66
70
  def sync
71
+ pending_keys = @pending_keys.dup
67
72
  @dataloader.yield
73
+ iterations = 0
74
+ while pending_keys.any? { |k| !@results.key?(k) }
75
+ iterations += 1
76
+ if iterations > 1000
77
+ raise "#{self.class}#sync tried 1000 times to load pending keys (#{pending_keys}), but they still weren't loaded. There is likely a circular dependency."
78
+ end
79
+ @dataloader.yield
80
+ end
81
+ nil
68
82
  end
69
83
 
70
84
  # @return [Boolean] True if this source has any pending requests for data.
71
85
  def pending?
72
- @pending_keys.any?
86
+ !@pending_keys.empty?
73
87
  end
74
88
 
75
89
  # Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
76
90
  # @api private
77
91
  # @return [void]
78
92
  def run_pending_keys
93
+ if !@fetching_keys.empty?
94
+ @pending_keys -= @fetching_keys
95
+ end
79
96
  return if @pending_keys.empty?
80
97
  fetch_keys = @pending_keys.uniq
98
+ @fetching_keys.concat(fetch_keys)
81
99
  @pending_keys = []
82
100
  results = fetch(fetch_keys)
83
101
  fetch_keys.each_with_index do |key, idx|
84
102
  @results[key] = results[idx]
85
103
  end
104
+ nil
86
105
  rescue StandardError => error
87
106
  fetch_keys.each { |key| @results[key] = error }
88
107
  ensure
89
- nil
108
+ if fetch_keys
109
+ @fetching_keys -= fetch_keys
110
+ end
90
111
  end
91
112
 
113
+ # These arguments are given to `dataloader.with(source_class, ...)`. The object
114
+ # returned from this method is used to de-duplicate batch loads under the hood
115
+ # by using it as a Hash key.
116
+ #
117
+ # By default, the arguments are all put in an Array. To customize how this source's
118
+ # batches are merged, override this method to return something else.
119
+ #
120
+ # For example, if you pass `ActiveRecord::Relation`s to `.with(...)`, you could override
121
+ # this method to call `.to_sql` on them, thus merging `.load(...)` calls when they apply
122
+ # to equivalent relations.
123
+ #
124
+ # @param batch_args [Array<Object>]
125
+ # @param batch_kwargs [Hash]
126
+ # @return [Object]
127
+ def self.batch_key_for(*batch_args, **batch_kwargs)
128
+ [*batch_args, **batch_kwargs]
129
+ end
130
+
131
+ attr_reader :pending_keys
132
+
92
133
  private
93
134
 
94
135
  # Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
@@ -96,6 +137,13 @@ module GraphQL
96
137
  # @return [Object] The result from {#fetch} for `key`.
97
138
  # @api private
98
139
  def result_for(key)
140
+ if !@results.key?(key)
141
+ raise <<-ERR
142
+ Invariant: fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys})
143
+
144
+ This key should have been loaded already. This is a bug in GraphQL::Dataloader, please report it on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new.
145
+ ERR
146
+ end
99
147
  result = @results[key]
100
148
 
101
149
  raise result if result.class <= StandardError
@@ -23,23 +23,42 @@ module GraphQL
23
23
  # end
24
24
  #
25
25
  class Dataloader
26
- def self.use(schema)
27
- schema.dataloader_class = self
26
+ class << self
27
+ attr_accessor :default_nonblocking
28
28
  end
29
29
 
30
- def initialize
31
- @source_cache = Hash.new { |h, source_class| h[source_class] = Hash.new { |h2, batch_parameters|
32
- source = if RUBY_VERSION < "3"
33
- source_class.new(*batch_parameters)
34
- else
35
- batch_args, batch_kwargs = batch_parameters
36
- source_class.new(*batch_args, **batch_kwargs)
37
- end
38
- source.setup(self)
39
- h2[batch_parameters] = source
40
- }
30
+ AsyncDataloader = Class.new(self) { self.default_nonblocking = true }
31
+
32
+ def self.use(schema, nonblocking: nil)
33
+ schema.dataloader_class = if nonblocking
34
+ AsyncDataloader
35
+ else
36
+ self
37
+ end
38
+ end
39
+
40
+ # Call the block with a Dataloader instance,
41
+ # then run all enqueued jobs and return the result of the block.
42
+ def self.with_dataloading(&block)
43
+ dataloader = self.new
44
+ result = nil
45
+ dataloader.append_job {
46
+ result = block.call(dataloader)
41
47
  }
48
+ dataloader.run
49
+ result
50
+ end
51
+
52
+ def initialize(nonblocking: self.class.default_nonblocking)
53
+ @source_cache = Hash.new { |h, k| h[k] = {} }
42
54
  @pending_jobs = []
55
+ if !nonblocking.nil?
56
+ @nonblocking = nonblocking
57
+ end
58
+ end
59
+
60
+ def nonblocking?
61
+ @nonblocking
43
62
  end
44
63
 
45
64
  # Get a Source instance from this dataloader, for calling `.load(...)` or `.request(...)` on.
@@ -48,17 +67,25 @@ module GraphQL
48
67
  # @param batch_parameters [Array<Object>]
49
68
  # @return [GraphQL::Dataloader::Source] An instance of {source_class}, initialized with `self, *batch_parameters`,
50
69
  # and cached for the lifetime of this {Multiplex}.
51
- if RUBY_VERSION < "3"
52
- def with(source_class, *batch_parameters)
53
- @source_cache[source_class][batch_parameters]
70
+ if RUBY_VERSION < "3" || RUBY_ENGINE != "ruby" # truffle-ruby wasn't doing well with the implementation below
71
+ def with(source_class, *batch_args)
72
+ batch_key = source_class.batch_key_for(*batch_args)
73
+ @source_cache[source_class][batch_key] ||= begin
74
+ source = source_class.new(*batch_args)
75
+ source.setup(self)
76
+ source
77
+ end
54
78
  end
55
79
  else
56
80
  def with(source_class, *batch_args, **batch_kwargs)
57
- batch_parameters = [batch_args, batch_kwargs]
58
- @source_cache[source_class][batch_parameters]
81
+ batch_key = source_class.batch_key_for(*batch_args, **batch_kwargs)
82
+ @source_cache[source_class][batch_key] ||= begin
83
+ source = source_class.new(*batch_args, **batch_kwargs)
84
+ source.setup(self)
85
+ source
86
+ end
59
87
  end
60
88
  end
61
-
62
89
  # Tell the dataloader that this fiber is waiting for data.
63
90
  #
64
91
  # Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
@@ -80,6 +107,16 @@ module GraphQL
80
107
  # Use a self-contained queue for the work in the block.
81
108
  def run_isolated
82
109
  prev_queue = @pending_jobs
110
+ prev_pending_keys = {}
111
+ @source_cache.each do |source_class, batched_sources|
112
+ batched_sources.each do |batch_args, batched_source_instance|
113
+ if batched_source_instance.pending?
114
+ prev_pending_keys[batched_source_instance] = batched_source_instance.pending_keys.dup
115
+ batched_source_instance.pending_keys.clear
116
+ end
117
+ end
118
+ end
119
+
83
120
  @pending_jobs = []
84
121
  res = nil
85
122
  # Make sure the block is inside a Fiber, so it can `Fiber.yield`
@@ -90,10 +127,16 @@ module GraphQL
90
127
  res
91
128
  ensure
92
129
  @pending_jobs = prev_queue
130
+ prev_pending_keys.each do |source_instance, pending_keys|
131
+ source_instance.pending_keys.concat(pending_keys)
132
+ end
93
133
  end
94
134
 
95
135
  # @api private Move along, move along
96
136
  def run
137
+ if @nonblocking && !Fiber.scheduler
138
+ raise "`nonblocking: true` requires `Fiber.scheduler`, assign one with `Fiber.set_scheduler(...)` before executing GraphQL."
139
+ end
97
140
  # At a high level, the algorithm is:
98
141
  #
99
142
  # A) Inside Fibers, run jobs from the queue one-by-one
@@ -114,6 +157,8 @@ module GraphQL
114
157
  #
115
158
  pending_fibers = []
116
159
  next_fibers = []
160
+ pending_source_fibers = []
161
+ next_source_fibers = []
117
162
  first_pass = true
118
163
 
119
164
  while first_pass || (f = pending_fibers.shift)
@@ -151,31 +196,27 @@ module GraphQL
151
196
  # This is where an evented approach would be even better -- can we tell which
152
197
  # fibers are ready to continue, and continue execution there?
153
198
  #
154
- source_fiber_queue = if (first_source_fiber = create_source_fiber)
155
- [first_source_fiber]
156
- else
157
- nil
199
+ if (first_source_fiber = create_source_fiber)
200
+ pending_source_fibers << first_source_fiber
158
201
  end
159
202
 
160
- if source_fiber_queue
161
- while (outer_source_fiber = source_fiber_queue.shift)
203
+ while pending_source_fibers.any?
204
+ while (outer_source_fiber = pending_source_fibers.pop)
162
205
  resume(outer_source_fiber)
163
-
164
- # If this source caused more sources to become pending, run those before running this one again:
165
- next_source_fiber = create_source_fiber
166
- if next_source_fiber
167
- source_fiber_queue << next_source_fiber
168
- end
169
-
170
206
  if outer_source_fiber.alive?
171
- source_fiber_queue << outer_source_fiber
207
+ next_source_fibers << outer_source_fiber
208
+ end
209
+ if (next_source_fiber = create_source_fiber)
210
+ pending_source_fibers << next_source_fiber
172
211
  end
173
212
  end
213
+ join_queues(pending_source_fibers, next_source_fibers)
214
+ next_source_fibers.clear
174
215
  end
175
216
  # Move newly-enqueued Fibers on to the list to be resumed.
176
217
  # Clear out the list of next-round Fibers, so that
177
218
  # any Fibers that pause can be put on it.
178
- pending_fibers.concat(next_fibers)
219
+ join_queues(pending_fibers, next_fibers)
179
220
  next_fibers.clear
180
221
  end
181
222
  end
@@ -190,6 +231,14 @@ module GraphQL
190
231
  nil
191
232
  end
192
233
 
234
+ def join_queues(previous_queue, next_queue)
235
+ if @nonblocking
236
+ Fiber.scheduler.run
237
+ next_queue.select!(&:alive?)
238
+ end
239
+ previous_queue.concat(next_queue)
240
+ end
241
+
193
242
  private
194
243
 
195
244
  # If there are pending sources, return a fiber for running them.
@@ -243,9 +292,16 @@ module GraphQL
243
292
  fiber_locals[fiber_var_key] = Thread.current[fiber_var_key]
244
293
  end
245
294
 
246
- Fiber.new do
247
- fiber_locals.each { |k, v| Thread.current[k] = v }
248
- yield
295
+ if @nonblocking
296
+ Fiber.new(blocking: false) do
297
+ fiber_locals.each { |k, v| Thread.current[k] = v }
298
+ yield
299
+ end
300
+ else
301
+ Fiber.new do
302
+ fiber_locals.each { |k, v| Thread.current[k] = v }
303
+ yield
304
+ end
249
305
  end
250
306
  end
251
307
  end
@@ -76,7 +76,7 @@ ERR
76
76
  # Apply definition from `define(...)` kwargs
77
77
  defn.define_keywords.each do |keyword, value|
78
78
  # Don't splat string hashes, which blows up on Rubies before 2.7
79
- if value.is_a?(Hash) && value.each_key.all? { |k| k.is_a?(Symbol) }
79
+ if value.is_a?(Hash) && !value.empty? && value.each_key.all? { |k| k.is_a?(Symbol) }
80
80
  defn_proxy.public_send(keyword, **value)
81
81
  else
82
82
  defn_proxy.public_send(keyword, value)
@@ -38,9 +38,17 @@ module GraphQL
38
38
  end
39
39
  end
40
40
 
41
- TYPE_CLASSES.each do |type_class|
42
- refine type_class.singleton_class do
43
- include Methods
41
+ if defined?(::Refinement) && Refinement.private_method_defined?(:import_methods)
42
+ TYPE_CLASSES.each do |type_class|
43
+ refine type_class.singleton_class do
44
+ import_methods Methods
45
+ end
46
+ end
47
+ else
48
+ TYPE_CLASSES.each do |type_class|
49
+ refine type_class.singleton_class do
50
+ include Methods
51
+ end
44
52
  end
45
53
  end
46
54
  end
@@ -3,11 +3,7 @@
3
3
  module GraphQL
4
4
  module Deprecation
5
5
  def self.warn(message)
6
- if defined?(ActiveSupport::Deprecation)
7
- ActiveSupport::Deprecation.warn(message)
8
- else
9
- Kernel.warn(message)
10
- end
6
+ Kernel.warn(message)
11
7
  end
12
8
  end
13
9
  end
@@ -1,2 +1,2 @@
1
1
  # frozen_string_literal: true
2
- GraphQL::Directive::DeprecatedDirective = GraphQL::Schema::Directive::Deprecated.graphql_definition
2
+ GraphQL::Directive::DeprecatedDirective = GraphQL::Schema::Directive::Deprecated.graphql_definition(silence_deprecation_warning: true)
@@ -1,2 +1,2 @@
1
1
  # frozen_string_literal: true
2
- GraphQL::Directive::IncludeDirective = GraphQL::Schema::Directive::Include.graphql_definition
2
+ GraphQL::Directive::IncludeDirective = GraphQL::Schema::Directive::Include.graphql_definition(silence_deprecation_warning: true)
@@ -1,2 +1,2 @@
1
1
  # frozen_string_literal: true
2
- GraphQL::Directive::SkipDirective = GraphQL::Schema::Directive::Skip.graphql_definition
2
+ GraphQL::Directive::SkipDirective = GraphQL::Schema::Directive::Skip.graphql_definition(silence_deprecation_warning: true)
@@ -105,7 +105,3 @@ module GraphQL
105
105
  end
106
106
  end
107
107
  end
108
-
109
- require "graphql/directive/include_directive"
110
- require "graphql/directive/skip_directive"
111
- require "graphql/directive/deprecated_directive"