graphql 1.12.5 → 1.12.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +1 -1
  3. data/lib/generators/graphql/templates/graphql_controller.erb +2 -2
  4. data/lib/graphql.rb +13 -11
  5. data/lib/graphql/dataloader.rb +36 -5
  6. data/lib/graphql/execution/errors.rb +109 -11
  7. data/lib/graphql/execution/interpreter/runtime.rb +32 -36
  8. data/lib/graphql/introspection.rb +1 -1
  9. data/lib/graphql/introspection/directive_type.rb +7 -3
  10. data/lib/graphql/language.rb +1 -0
  11. data/lib/graphql/language/cache.rb +37 -0
  12. data/lib/graphql/language/parser.rb +15 -5
  13. data/lib/graphql/language/parser.y +15 -5
  14. data/lib/graphql/pagination/active_record_relation_connection.rb +7 -0
  15. data/lib/graphql/pagination/connection.rb +6 -1
  16. data/lib/graphql/pagination/connections.rb +2 -1
  17. data/lib/graphql/pagination/relation_connection.rb +12 -1
  18. data/lib/graphql/query.rb +1 -3
  19. data/lib/graphql/query/null_context.rb +7 -1
  20. data/lib/graphql/query/validation_pipeline.rb +1 -1
  21. data/lib/graphql/railtie.rb +9 -1
  22. data/lib/graphql/rake_task.rb +3 -0
  23. data/lib/graphql/schema.rb +23 -37
  24. data/lib/graphql/schema/argument.rb +3 -1
  25. data/lib/graphql/schema/field/connection_extension.rb +1 -0
  26. data/lib/graphql/schema/input_object.rb +2 -2
  27. data/lib/graphql/schema/loader.rb +8 -0
  28. data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
  29. data/lib/graphql/schema/object.rb +19 -5
  30. data/lib/graphql/schema/resolver.rb +24 -22
  31. data/lib/graphql/schema/scalar.rb +3 -1
  32. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +3 -1
  33. data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +6 -2
  34. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  35. data/lib/graphql/static_validation/rules/arguments_are_defined_error.rb +4 -2
  36. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
  37. data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
  38. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  39. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +2 -2
  40. data/lib/graphql/static_validation/validator.rb +5 -0
  41. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  42. data/lib/graphql/subscriptions/broadcast_analyzer.rb +0 -3
  43. data/lib/graphql/subscriptions/serialize.rb +11 -1
  44. data/lib/graphql/tracing/active_support_notifications_tracing.rb +2 -1
  45. data/lib/graphql/types/relay/base_connection.rb +4 -0
  46. data/lib/graphql/types/relay/connection_behaviors.rb +38 -5
  47. data/lib/graphql/types/relay/edge_behaviors.rb +12 -1
  48. data/lib/graphql/version.rb +1 -1
  49. metadata +7 -6
@@ -12,13 +12,17 @@ module GraphQL
12
12
  field :name, String, null: false, method: :graphql_name
13
13
  field :description, String, null: true
14
14
  field :locations, [GraphQL::Schema::LateBoundType.new("__DirectiveLocation")], null: false
15
- field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false
15
+ field :args, [GraphQL::Schema::LateBoundType.new("__InputValue")], null: false do
16
+ argument :include_deprecated, Boolean, required: false, default_value: false
17
+ end
16
18
  field :on_operation, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_operation?
17
19
  field :on_fragment, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_fragment?
18
20
  field :on_field, Boolean, null: false, deprecation_reason: "Use `locations`.", method: :on_field?
19
21
 
20
- def args
21
- @context.warden.arguments(@object)
22
+ def args(include_deprecated:)
23
+ args = @context.warden.arguments(@object)
24
+ args = args.reject(&:deprecation_reason) unless include_deprecated
25
+ args
22
26
  end
23
27
  end
24
28
  end
@@ -6,6 +6,7 @@ require "graphql/language/document_from_schema_definition"
6
6
  require "graphql/language/generation"
7
7
  require "graphql/language/lexer"
8
8
  require "graphql/language/nodes"
9
+ require "graphql/language/cache"
9
10
  require "graphql/language/parser"
10
11
  require "graphql/language/token"
11
12
  require "graphql/language/visitor"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql/version'
4
+ require 'digest/sha2'
5
+
6
+ module GraphQL
7
+ module Language
8
+ class Cache
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ DIGEST = Digest::SHA256.new << GraphQL::VERSION
14
+ def fetch(filename)
15
+ hash = DIGEST.dup << filename
16
+ begin
17
+ hash << File.mtime(filename).to_i.to_s
18
+ rescue SystemCallError
19
+ return yield
20
+ end
21
+ cache_path = @path.join(hash.to_s)
22
+
23
+ if cache_path.exist?
24
+ Marshal.load(cache_path.read)
25
+ else
26
+ payload = yield
27
+ tmp_path = "#{cache_path}.#{rand}"
28
+
29
+ @path.mkpath
30
+ File.binwrite(tmp_path, Marshal.dump(payload))
31
+ File.rename(tmp_path, cache_path.to_s)
32
+ payload
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -41,12 +41,22 @@ def parse_document
41
41
  end
42
42
  end
43
43
 
44
- def self.parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
45
- self.new(query_string, filename: filename, tracer: tracer).parse_document
46
- end
44
+ class << self
45
+ attr_accessor :cache
46
+
47
+ def parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
48
+ new(query_string, filename: filename, tracer: tracer).parse_document
49
+ end
47
50
 
48
- def self.parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
49
- self.parse(File.read(filename), filename: filename, tracer: tracer)
51
+ def parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
52
+ if cache
53
+ cache.fetch(filename) do
54
+ parse(File.read(filename), filename: filename, tracer: tracer)
55
+ end
56
+ else
57
+ parse(File.read(filename), filename: filename, tracer: tracer)
58
+ end
59
+ end
50
60
  end
51
61
 
52
62
  private
@@ -462,12 +462,22 @@ def parse_document
462
462
  end
463
463
  end
464
464
 
465
- def self.parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
466
- self.new(query_string, filename: filename, tracer: tracer).parse_document
467
- end
465
+ class << self
466
+ attr_accessor :cache
467
+
468
+ def parse(query_string, filename: nil, tracer: GraphQL::Tracing::NullTracer)
469
+ new(query_string, filename: filename, tracer: tracer).parse_document
470
+ end
468
471
 
469
- def self.parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
470
- self.parse(File.read(filename), filename: filename, tracer: tracer)
472
+ def parse_file(filename, tracer: GraphQL::Tracing::NullTracer)
473
+ if cache
474
+ cache.fetch(filename) do
475
+ parse(File.read(filename), filename: filename, tracer: tracer)
476
+ end
477
+ else
478
+ parse(File.read(filename), filename: filename, tracer: tracer)
479
+ end
480
+ end
471
481
  end
472
482
 
473
483
  private
@@ -5,6 +5,13 @@ module GraphQL
5
5
  module Pagination
6
6
  # Customizes `RelationConnection` to work with `ActiveRecord::Relation`s.
7
7
  class ActiveRecordRelationConnection < Pagination::RelationConnection
8
+ private
9
+
10
+ def relation_larger_than(relation, size)
11
+ initial_offset = relation.offset_value || 0
12
+ relation.offset(initial_offset + size).exists?
13
+ end
14
+
8
15
  def relation_count(relation)
9
16
  int_or_hash = if relation.respond_to?(:unscope)
10
17
  relation.unscope(:order).count(:all)
@@ -45,6 +45,9 @@ module GraphQL
45
45
  end
46
46
  end
47
47
 
48
+ # @return [Hash<Symbol => Object>] The field arguments from the field that returned this connection
49
+ attr_accessor :arguments
50
+
48
51
  # @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation`
49
52
  # @param context [Query::Context]
50
53
  # @param parent [Object] The object this collection belongs to
@@ -52,8 +55,9 @@ module GraphQL
52
55
  # @param after [String, nil] A cursor for pagination, if the client provided one
53
56
  # @param last [Integer, nil] Limit parameter from the client, if provided
54
57
  # @param before [String, nil] A cursor for pagination, if the client provided one.
58
+ # @param arguments [Hash] The arguments to the field that returned the collection wrapped by this connection
55
59
  # @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given.
56
- def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil, edge_class: nil)
60
+ def initialize(items, parent: nil, field: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil, edge_class: nil, arguments: nil)
57
61
  @items = items
58
62
  @parent = parent
59
63
  @context = context
@@ -62,6 +66,7 @@ module GraphQL
62
66
  @after_value = after
63
67
  @last_value = last
64
68
  @before_value = before
69
+ @arguments = arguments
65
70
  @edge_class = edge_class || self.class::Edge
66
71
  # This is only true if the object was _initialized_ with an override
67
72
  # or if one is assigned later.
@@ -79,11 +79,12 @@ module GraphQL
79
79
  context: context,
80
80
  parent: parent,
81
81
  field: field,
82
- max_page_size: field.max_page_size || context.schema.default_max_page_size,
82
+ max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
83
83
  first: arguments[:first],
84
84
  after: arguments[:after],
85
85
  last: arguments[:last],
86
86
  before: arguments[:before],
87
+ arguments: arguments,
87
88
  edge_class: edge_class_for_field(field),
88
89
  )
89
90
  end
@@ -32,7 +32,11 @@ module GraphQL
32
32
  @has_next_page = if before_offset && before_offset > 0
33
33
  true
34
34
  elsif first
35
- relation_count(set_limit(sliced_nodes, first + 1)) == first + 1
35
+ if @nodes && @nodes.count < first
36
+ false
37
+ else
38
+ relation_larger_than(sliced_nodes, first)
39
+ end
36
40
  else
37
41
  false
38
42
  end
@@ -49,6 +53,13 @@ module GraphQL
49
53
 
50
54
  private
51
55
 
56
+ # @param relation [Object] A database query object
57
+ # @param size [Integer] The value against which we check the relation size
58
+ # @return [Boolean] True if the number of items in this relation is larger than `size`
59
+ def relation_larger_than(relation, size)
60
+ relation_count(set_limit(relation, size + 1)) == size + 1
61
+ end
62
+
52
63
  # @param relation [Object] A database query object
53
64
  # @return [Integer, nil] The offset value, or nil if there isn't one
54
65
  def relation_offset(relation)
data/lib/graphql/query.rb CHANGED
@@ -195,9 +195,7 @@ module GraphQL
195
195
  # @return [Hash] A GraphQL response, with `"data"` and/or `"errors"` keys
196
196
  def result
197
197
  if !@executed
198
- with_prepared_ast {
199
- Execution::Multiplex.run_queries(@schema, [self], context: @context)
200
- }
198
+ Execution::Multiplex.run_queries(@schema, [self], context: @context)
201
199
  end
202
200
  @result ||= Query::Result.new(query: self, values: @result_values)
203
201
  end
@@ -9,10 +9,16 @@ module GraphQL
9
9
  def visible_type?(t); true; end
10
10
  end
11
11
 
12
+ class NullQuery
13
+ def with_error_handling
14
+ yield
15
+ end
16
+ end
17
+
12
18
  attr_reader :schema, :query, :warden, :dataloader
13
19
 
14
20
  def initialize
15
- @query = nil
21
+ @query = NullQuery.new
16
22
  @dataloader = GraphQL::Dataloader::NullDataloader.new
17
23
  @schema = GraphQL::Schema.new
18
24
  @warden = NullWarden.new(
@@ -36,7 +36,7 @@ module GraphQL
36
36
  @valid
37
37
  end
38
38
 
39
- # @return [Array<GraphQL::StaticValidation::Error >] Static validation errors for the query string
39
+ # @return [Array<GraphQL::StaticValidation::Error, GraphQL::Query::VariableValidationError>] Static validation errors for the query string
40
40
  def validation_errors
41
41
  ensure_has_validated
42
42
  @validation_errors
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  module GraphQL
5
4
  class Railtie < Rails::Railtie
5
+ config.before_configuration do
6
+ # Bootsnap compile cache has similar expiration properties,
7
+ # so we assume that if the user has bootsnap setup it's ok
8
+ # to piggy back on it.
9
+ if ::Object.const_defined?("Bootsnap::CompileCache::ISeq") && Bootsnap::CompileCache::ISeq.cache_dir
10
+ Language::Parser.cache ||= Language::Cache.new(Pathname.new(Bootsnap::CompileCache::ISeq.cache_dir).join('graphql'))
11
+ end
12
+ end
13
+
6
14
  rake_tasks do
7
15
  # Defer this so that you only need the `parser` gem when you _run_ the upgrader
8
16
  def load_upgraders
@@ -98,6 +98,9 @@ module GraphQL
98
98
  result = schema.public_send(method_name, only: @only, except: @except, context: context)
99
99
  dir = File.dirname(file)
100
100
  FileUtils.mkdir_p(dir)
101
+ if !result.end_with?("\n")
102
+ result += "\n"
103
+ end
101
104
  File.write(file, result)
102
105
  end
103
106
 
@@ -355,23 +355,6 @@ module GraphQL
355
355
  # For forwards-compatibility with Schema classes
356
356
  alias :graphql_definition :itself
357
357
 
358
- # Validate a query string according to this schema.
359
- # @param string_or_document [String, GraphQL::Language::Nodes::Document]
360
- # @return [Array<GraphQL::StaticValidation::Error >]
361
- def validate(string_or_document, rules: nil, context: nil)
362
- doc = if string_or_document.is_a?(String)
363
- GraphQL.parse(string_or_document)
364
- else
365
- string_or_document
366
- end
367
- query = GraphQL::Query.new(self, document: doc, context: context)
368
- validator_opts = { schema: self }
369
- rules && (validator_opts[:rules] = rules)
370
- validator = GraphQL::StaticValidation::Validator.new(**validator_opts)
371
- res = validator.validate(query, timeout: validate_timeout)
372
- res[:errors]
373
- end
374
-
375
358
  def deprecated_define(**kwargs, &block)
376
359
  super
377
360
  ensure_defined
@@ -711,7 +694,8 @@ module GraphQL
711
694
  alias :_schema_class :class
712
695
  def_delegators :_schema_class, :unauthorized_object, :unauthorized_field, :inaccessible_fields
713
696
  def_delegators :_schema_class, :directive
714
- def_delegators :_schema_class, :error_handler, :rescues
697
+ def_delegators :_schema_class, :error_handler
698
+ def_delegators :_schema_class, :validate
715
699
 
716
700
 
717
701
  # Given this schema member, find the class-based definition object
@@ -861,7 +845,6 @@ module GraphQL
861
845
  def_delegators :graphql_definition,
862
846
  # Execution
863
847
  :execution_strategy_for_operation,
864
- :validate,
865
848
  # Configuration
866
849
  :metadata, :redefine,
867
850
  :id_from_object_proc, :object_from_id_proc,
@@ -989,7 +972,7 @@ module GraphQL
989
972
  schema_defn.lazy_methods.set(lazy_class, value_method)
990
973
  end
991
974
 
992
- rescues.each do |err_class, handler|
975
+ error_handler.each_rescue do |err_class, handler|
993
976
  schema_defn.rescue_from(err_class, &handler)
994
977
  end
995
978
 
@@ -1293,6 +1276,23 @@ module GraphQL
1293
1276
  end
1294
1277
  end
1295
1278
 
1279
+ # Validate a query string according to this schema.
1280
+ # @param string_or_document [String, GraphQL::Language::Nodes::Document]
1281
+ # @return [Array<GraphQL::StaticValidation::Error >]
1282
+ def validate(string_or_document, rules: nil, context: nil)
1283
+ doc = if string_or_document.is_a?(String)
1284
+ GraphQL.parse(string_or_document)
1285
+ else
1286
+ string_or_document
1287
+ end
1288
+ query = GraphQL::Query.new(self, document: doc, context: context)
1289
+ validator_opts = { schema: self }
1290
+ rules && (validator_opts[:rules] = rules)
1291
+ validator = GraphQL::StaticValidation::Validator.new(**validator_opts)
1292
+ res = validator.validate(query, timeout: validate_timeout)
1293
+ res[:errors]
1294
+ end
1295
+
1296
1296
  attr_writer :max_complexity
1297
1297
 
1298
1298
  def max_complexity(max_complexity = nil)
@@ -1424,7 +1424,7 @@ module GraphQL
1424
1424
 
1425
1425
  def rescue_from(*err_classes, &handler_block)
1426
1426
  err_classes.each do |err_class|
1427
- own_rescues[err_class] = handler_block
1427
+ error_handler.rescue_from(err_class, handler_block)
1428
1428
  end
1429
1429
  end
1430
1430
 
@@ -1468,10 +1468,6 @@ module GraphQL
1468
1468
  super
1469
1469
  end
1470
1470
 
1471
- def rescues
1472
- find_inherited_value(:rescues, EMPTY_HASH).merge(own_rescues)
1473
- end
1474
-
1475
1471
  def object_from_id(node_id, ctx)
1476
1472
  raise GraphQL::RequiredImplementationMissingError, "#{self.name}.object_from_id(node_id, ctx) must be implemented to load by ID (tried to load from id `#{node_id}`)"
1477
1473
  end
@@ -1548,15 +1544,10 @@ module GraphQL
1548
1544
  def parse_error(parse_err, ctx)
1549
1545
  ctx.errors.push(parse_err)
1550
1546
  end
1551
- attr_writer :error_handler
1552
1547
 
1553
- # @return [GraphQL::Execution::Errors, Class<GraphQL::Execution::Errors::NullErrorHandler>]
1548
+ # @return [GraphQL::Execution::Errors]
1554
1549
  def error_handler
1555
- if defined?(@error_handler)
1556
- @error_handler
1557
- else
1558
- find_inherited_value(:error_handler, GraphQL::Execution::Errors::NullErrorHandler)
1559
- end
1550
+ @error_handler ||= GraphQL::Execution::Errors.new(self)
1560
1551
  end
1561
1552
 
1562
1553
  def lazy_resolve(lazy_class, value_method)
@@ -1744,10 +1735,6 @@ module GraphQL
1744
1735
  @own_plugins ||= []
1745
1736
  end
1746
1737
 
1747
- def own_rescues
1748
- @own_rescues ||= {}
1749
- end
1750
-
1751
1738
  def own_orphan_types
1752
1739
  @own_orphan_types ||= []
1753
1740
  end
@@ -1990,7 +1977,6 @@ module GraphQL
1990
1977
  end
1991
1978
 
1992
1979
  # Install these here so that subclasses will also install it.
1993
- use(GraphQL::Execution::Errors)
1994
1980
  use(GraphQL::Pagination::Connections)
1995
1981
 
1996
1982
  protected
@@ -266,7 +266,9 @@ module GraphQL
266
266
  loaded_values = coerced_value.map { |val| owner.load_application_object(self, loads, val, context) }
267
267
  context.schema.after_any_lazies(loaded_values) { |result| result }
268
268
  else
269
- owner.load_application_object(self, loads, coerced_value, context)
269
+ context.query.with_error_handling do
270
+ owner.load_application_object(self, loads, coerced_value, context)
271
+ end
270
272
  end
271
273
  end
272
274
 
@@ -42,6 +42,7 @@ module GraphQL
42
42
  value.after_value ||= original_arguments[:after]
43
43
  value.last_value ||= original_arguments[:last]
44
44
  value.before_value ||= original_arguments[:before]
45
+ value.arguments ||= original_arguments
45
46
  value.field ||= field
46
47
  if field.has_max_page_size? && !value.has_max_page_size_override?
47
48
  value.max_page_size = field.max_page_size
@@ -226,8 +226,8 @@ module GraphQL
226
226
  # It's funny to think of a _result_ of an input object.
227
227
  # This is used for rendering the default value in introspection responses.
228
228
  def coerce_result(value, ctx)
229
- # Allow the application to provide values as :symbols, and convert them to the strings
230
- value = value.reduce({}) { |memo, (k, v)| memo[k.to_s] = v; memo }
229
+ # Allow the application to provide values as :snake_symbols, and convert them to the camelStrings
230
+ value = value.reduce({}) { |memo, (k, v)| memo[Member::BuildType.camelize(k.to_s)] = v; memo }
231
231
 
232
232
  result = {}
233
233