graphql 2.6.7 → 2.6.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75192ceea307d33645d2d511db98530f8021c8af45af33fc75e86dcd69f53952
4
- data.tar.gz: ff3ee9e77876ad45fb67491fb5076df4614ab086ecfa15e476b3d35bca2e3b22
3
+ metadata.gz: 6a359d53729477046ffd6b51ef9345099defcdc95521579711fffe5ab79c0b5e
4
+ data.tar.gz: 4c88aecd90fb26e02198f21f4bdf7a73f577ec37ac9ea9eb6c840c8a2957fc7a
5
5
  SHA512:
6
- metadata.gz: 50bc8591ece79fc8070b4c3a6a3611f29e9a4ad84a94e6f310a5fd2c00ac0d10b6db5ebe44474e1d5f002a76615d6a25dd4dbfc2a8f56ec1c2d0d88972ce95e2
7
- data.tar.gz: b28b83923ac25bb605d7fe1e9b25a64e37aa8173a64c958849287cdd0c87cc419deea25f36b4d525a6ea17088b7a02cb0a51011750c568337a6274b0373ff43b
6
+ metadata.gz: d40bb56d8b5e77ca6f126f1528e2a074bbf679996dfb8ea5a12d646d028d547848dc36dea3a5fb73273fb7252135a5c7e904ec7d0a38790fe05164c5e81701b5
7
+ data.tar.gz: e486b9492693cdc69375f8d3bc662e075a71ff45642f62cb2fd741959b2a2d1f9105fa14718b992ef2d3b3a809fd0f0d94ec66abf509137b3a7af05877527c23
@@ -26,7 +26,8 @@ class <%= schema_name %> < GraphQL::Schema
26
26
  raise(GraphQL::RequiredImplementationMissingError)
27
27
  end
28
28
 
29
- # Limit the size of incoming queries:
29
+ # Limit the depth and size of incoming queries:
30
+ max_depth(15)
30
31
  max_query_string_tokens(5000)
31
32
 
32
33
  # Stop validating when it encounters this many errors:
@@ -22,7 +22,11 @@ module Graphql
22
22
 
23
23
  def schema_class
24
24
  @schema_class ||= begin
25
- schema_param = request.query_parameters["schema"] || params[:schema]
25
+ configured_schemas = Array(request.path_parameters[:schema] || request.path_parameters["schema"])
26
+ schema_param = request.query_parameters["schema"]
27
+ schema_param = configured_schemas.find { |schema| schema.to_s == schema_param } if schema_param
28
+ schema_param ||= configured_schemas.first
29
+
26
30
  case schema_param
27
31
  when Class
28
32
  schema_param
@@ -10,6 +10,9 @@ module Graphql
10
10
  # @example Mounting the Dashboard in your app
11
11
  # mount GraphQL::Dashboard, at: "graphql_dashboard", schema: "MySchema"
12
12
  #
13
+ # Pass an array to allow selecting from multiple schemas with the `schema` query parameter.
14
+ # mount GraphQL::Dashboard, at: "graphql_dashboard", schema: ["MySchema", "OtherSchema"]
15
+ #
13
16
  # @example Authenticating the Dashboard with HTTP Basic Auth
14
17
  # # config/initializers/graphql_dashboard.rb
15
18
  # GraphQL::Dashboard.middleware.use(Rack::Auth::Basic) do |username, password|
@@ -130,7 +130,7 @@ module GraphQL
130
130
  highest_nulled_depth = path.size
131
131
  highest_list_depth = nil
132
132
  current_field_step = self
133
- while current_field_step
133
+ while current_field_step && current_field_step.field_definition
134
134
  return_type = current_field_step.field_definition.type
135
135
  if propagating_null && return_type.non_null?
136
136
  highest_nulled_depth = current_field_step.path.size
@@ -147,7 +147,7 @@ module GraphQL
147
147
 
148
148
  if highest_list_depth.nil? || highest_nulled_depth <= highest_list_depth
149
149
  kill_field_step = self
150
- while kill_field_step && highest_nulled_depth <= kill_field_step.path.size
150
+ while kill_field_step && kill_field_step.field_definition && highest_nulled_depth <= kill_field_step.path.size
151
151
  kill_field_step.selections_step.killed = true
152
152
  kill_field_step = kill_field_step.selections_step.field_resolve_step
153
153
  end
@@ -119,6 +119,12 @@ module GraphQL
119
119
  end
120
120
 
121
121
  results
122
+ rescue SystemStackError => err
123
+ queries.map do |query|
124
+ schema.query_stack_error(query, err)
125
+ query.result_values ||= { "errors" => query.context.errors.map(&:to_h) }
126
+ query.result
127
+ end
122
128
  rescue Exception
123
129
  # TODO rescue at a higher level so it will catch errors in analysis, too
124
130
  # Assign values here so that the query's `@executed` becomes true
@@ -5,7 +5,6 @@ module GraphQL
5
5
  def initialize(multiplex)
6
6
  @multiplex = multiplex
7
7
  @schema = multiplex.schema
8
- @steps_queue = []
9
8
  @runtime_type_at = {}.compare_by_identity
10
9
  @static_type_at = {}.compare_by_identity
11
10
  @finalizers = nil
@@ -53,7 +52,7 @@ module GraphQL
53
52
  @dataloader.append_job(step)
54
53
  end
55
54
 
56
- attr_reader :steps_queue, :schema, :variables, :dataloader, :resolves_lazies, :authorizes, :static_type_at, :runtime_type_at, :finalizers, :input_values
55
+ attr_reader :schema, :variables, :dataloader, :resolves_lazies, :authorizes, :static_type_at, :runtime_type_at, :finalizers, :input_values
57
56
 
58
57
  # @return [void]
59
58
  def add_finalizer(query, result_value, key, finalizer)
@@ -21,17 +21,12 @@ module GraphQL
21
21
  next
22
22
  end
23
23
  line_length = line.size
24
- line_indent = if line.match?(/\A [^ ]/)
25
- 2
26
- elsif line.match?(/\A [^ ]/)
27
- 4
28
- elsif line.match?(/\A[^ ]/)
29
- 0
30
- else
31
- line[/\A */].size
24
+ leading = 0
25
+ while leading < line_length && line.getbyte(leading) == 32
26
+ leading += 1
32
27
  end
33
- if line_indent < line_length && (common_indent.nil? || line_indent < common_indent)
34
- common_indent = line_indent
28
+ if leading < line_length && (common_indent.nil? || leading < common_indent)
29
+ common_indent = leading
35
30
  end
36
31
  end
37
32
 
@@ -41,7 +36,7 @@ module GraphQL
41
36
  if idx == 0
42
37
  next
43
38
  else
44
- line.slice!(0, common_indent)
39
+ line[0, common_indent] = ""
45
40
  end
46
41
  end
47
42
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'graphql/version'
4
4
  require 'digest/sha2'
5
+ require 'openssl'
6
+ require 'securerandom'
7
+ require 'tempfile'
5
8
 
6
9
  module GraphQL
7
10
  module Language
@@ -9,7 +12,11 @@ module GraphQL
9
12
  #
10
13
  # With Rails, parser caching may enabled by setting `config.graphql.parser_cache = true` in your Rails application.
11
14
  #
12
- # The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new("some_dir")`.
15
+ # The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new(Pathname.new("some_dir"), secret: ENV.fetch("GRAPHQL_CACHE_SECRET"))`.
16
+ # The `secret` should be a stable value stored outside of the cache directory.
17
+ # When it isn't provided, a process-local secret is generated and cache entries
18
+ # are rebuilt after the process restarts. Pass `secret: nil` to disable cache
19
+ # signing. This should only be used when the cache directory is trusted.
13
20
  # This will create a directory (`tmp/cache/graphql` by default) that stores a cache of parsed files.
14
21
  #
15
22
  # Much like [bootsnap](https://github.com/Shopify/bootsnap), the parser cache needs to be cleaned up manually.
@@ -18,32 +25,95 @@ module GraphQL
18
25
  #
19
26
  # @see GraphQL::Railtie for simple Rails integration
20
27
  class Cache
21
- def initialize(path)
28
+ # @param path [Pathname] The directory where cache entries are stored.
29
+ # @param secret [String, nil] A stable secret for verifying cache entries. When omitted,
30
+ # a process-local secret is generated. Pass `nil` to disable cache signing.
31
+ def initialize(path, secret: SecureRandom.random_bytes(32))
22
32
  @path = path
33
+ @secret = secret
23
34
  end
24
35
 
25
36
  DIGEST = Digest::SHA256.new << GraphQL::VERSION
37
+ HMAC_SIZE = OpenSSL::Digest::SHA256.new.digest_length
38
+ InvalidCache = Class.new(StandardError)
39
+ private_constant :InvalidCache
26
40
 
27
41
  def fetch(filename)
28
- hash = DIGEST.dup << filename
42
+ cache_key = cache_key_for(filename)
43
+ return yield unless cache_key
44
+
45
+ cache_path = @path.join(cache_key)
46
+
47
+ begin
48
+ return load_cache(cache_path, cache_key) if cache_path.file?
49
+ rescue InvalidCache, SystemCallError
50
+ # Rebuild caches created by older versions or with an invalid signature.
51
+ end
52
+
53
+ payload = yield
29
54
  begin
30
- hash << File.mtime(filename).to_i.to_s
55
+ write_cache(cache_path, cache_key, payload)
31
56
  rescue SystemCallError
32
- return yield
57
+ # Parser caching is best-effort; return the parsed payload if the cache cannot be written.
33
58
  end
34
- cache_path = @path.join(hash.to_s)
59
+ payload
60
+ end
61
+
62
+ private
63
+
64
+ def cache_key_for(filename)
65
+ content_digest = Digest::SHA256.file(filename).hexdigest
66
+ (DIGEST.dup << filename << content_digest).to_s
67
+ rescue SystemCallError
68
+ nil
69
+ end
70
+
71
+ def load_cache(cache_path, cache_key)
72
+ cache_data = cache_path.binread
73
+ return Marshal.load(cache_data) unless @secret
74
+
75
+ signature = cache_data.byteslice(0, HMAC_SIZE)
76
+ payload = cache_data.byteslice(HMAC_SIZE..-1)
77
+ raise InvalidCache unless signature && payload
35
78
 
36
- if cache_path.exist?
37
- Marshal.load(cache_path.read)
79
+ expected_signature = signature_for(cache_key, payload)
80
+ unless secure_compare(signature, expected_signature)
81
+ raise InvalidCache
82
+ end
83
+ Marshal.load(payload)
84
+ end
85
+
86
+ def write_cache(cache_path, cache_key, payload)
87
+ @path.mkpath
88
+ serialized_payload = Marshal.dump(payload)
89
+ cache_data = if @secret
90
+ signature_for(cache_key, serialized_payload) + serialized_payload
38
91
  else
39
- payload = yield
40
- tmp_path = "#{cache_path}.#{rand}"
92
+ serialized_payload
93
+ end
94
+
95
+ Tempfile.create(['graphql-cache-', '.tmp'], @path.to_s) do |tempfile|
96
+ tempfile.binmode
97
+ tempfile.write(cache_data)
98
+ tempfile.flush
99
+ tempfile.fsync
100
+ tempfile.close
101
+ File.rename(tempfile.path, cache_path.to_s)
102
+ end
103
+ end
104
+
105
+ def signature_for(cache_key, payload)
106
+ OpenSSL::HMAC.digest('SHA256', @secret, cache_key + payload)
107
+ end
108
+
109
+ def secure_compare(left, right)
110
+ return false unless left.bytesize == right.bytesize
41
111
 
42
- @path.mkpath
43
- File.binwrite(tmp_path, Marshal.dump(payload))
44
- File.rename(tmp_path, cache_path.to_s)
45
- payload
112
+ result = 0
113
+ left.bytes.each_with_index do |byte, index|
114
+ result |= byte ^ right.getbyte(index)
46
115
  end
116
+ result.zero?
47
117
  end
48
118
  end
49
119
  end
@@ -175,11 +175,14 @@ module GraphQL
175
175
  end
176
176
 
177
177
  def line_number
178
- @scanner.string[0..@pos].count("\n") + 1
178
+ @scanner.string.byteslice(0, @pos).b.count("\n".b) + 1
179
179
  end
180
180
 
181
181
  def column_number
182
- @scanner.string[0..@pos].split("\n").last.length
182
+ line_prefix = @scanner.string.byteslice(0, @pos)
183
+ line_prefix = line_prefix.b unless line_prefix.valid_encoding?
184
+ newline_index = line_prefix.rindex("\n")
185
+ newline_index ? line_prefix.length - newline_index : line_prefix.length + 1
183
186
  end
184
187
 
185
188
  def raise_parse_error(message, line = line_number, col = column_number)
@@ -304,7 +304,8 @@ module GraphQL
304
304
  children_method_names.map { |m| "#{m}: NO_CHILDREN" } +
305
305
  DEFAULT_INITIALIZE_OPTIONS
306
306
 
307
- assignments = scalar_method_names.map { |m| "@#{m} = #{m}"} +
307
+ # Intern descriptions so identical strings across SDL documents share one frozen object.
308
+ assignments = scalar_method_names.map { |m| m == :description ? "@#{m} = #{m} && -#{m}" : "@#{m} = #{m}" } +
308
309
  children_method_names.map { |m| "@#{m} = #{m}.freeze" }
309
310
 
310
311
  if name.end_with?("Definition") && name != "FragmentDefinition"
@@ -31,7 +31,7 @@ module GraphQL
31
31
 
32
32
  "[#{serialized_array}]"
33
33
  else
34
- JSON.generate(value, quirks_mode: true)
34
+ JSON.generate(value)
35
35
  end
36
36
  rescue JSON::GeneratorError
37
37
  if Float::INFINITY == value
@@ -15,7 +15,8 @@ module GraphQL
15
15
  initializer("graphql.cache") do |app|
16
16
  if config.graphql.parser_cache
17
17
  Language::Parser.cache ||= Language::Cache.new(
18
- app.root.join("tmp/cache/graphql")
18
+ app.root.join("tmp/cache/graphql"),
19
+ secret: app.secret_key_base
19
20
  )
20
21
  end
21
22
  end
@@ -172,12 +172,12 @@ module GraphQL
172
172
  types = ctx.types
173
173
 
174
174
  if input.is_a?(Array)
175
- return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) })
175
+ return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input) })
176
176
  end
177
177
 
178
178
  if !(input.respond_to?(:to_h) || input.respond_to?(:to_unsafe_h))
179
179
  # We're not sure it'll act like a hash, so reject it:
180
- return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) })
180
+ return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input) })
181
181
  end
182
182
 
183
183
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "date"
2
3
  require "set"
3
4
  module GraphQL
4
5
  class Subscriptions
@@ -10,6 +11,7 @@ module GraphQL
10
11
  SYMBOL_KEYS_KEY = "__sym_keys__"
11
12
  TIMESTAMP_KEY = "__timestamp__"
12
13
  TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S.%N%z" # eg '2020-01-01 23:59:59.123456789+05:00'
14
+ TIMESTAMP_CLASS_NAMES = ["Date", "DateTime", "Time"].freeze
13
15
  OPEN_STRUCT_KEY = "__ostruct__"
14
16
 
15
17
  module_function
@@ -24,7 +26,7 @@ module GraphQL
24
26
  # @param obj [Object] Some subscription-related data to dump
25
27
  # @return [String] The stringified object
26
28
  def dump(obj)
27
- JSON.generate(dump_value(obj), quirks_mode: true)
29
+ JSON.generate(dump_value(obj))
28
30
  end
29
31
 
30
32
  # This is for turning objects into subscription scopes.
@@ -72,15 +74,19 @@ module GraphQL
72
74
  value[SYMBOL_KEY].to_sym
73
75
  when TIMESTAMP_KEY
74
76
  timestamp_class_name, *timestamp_args = value[TIMESTAMP_KEY]
75
- timestamp_class = Object.const_get(timestamp_class_name)
76
- if defined?(ActiveSupport::TimeWithZone) && timestamp_class <= ActiveSupport::TimeWithZone
77
+ timestamp_class = if TIMESTAMP_CLASS_NAMES.include?(timestamp_class_name)
78
+ Object.const_get(timestamp_class_name, false)
79
+ end
80
+ if defined?(ActiveSupport::TimeWithZone) && timestamp_class_name == ActiveSupport::TimeWithZone.name
77
81
  zone_name, timestamp_s = timestamp_args
78
82
  zone = ActiveSupport::TimeZone[zone_name]
79
83
  raise "Zone #{zone_name} not found, unable to deserialize" unless zone
80
84
  zone.strptime(timestamp_s, TIMESTAMP_FORMAT)
81
- else
85
+ elsif timestamp_class
82
86
  timestamp_s = timestamp_args.first
83
87
  timestamp_class.strptime(timestamp_s, TIMESTAMP_FORMAT)
88
+ else
89
+ raise ArgumentError, "Unsupported timestamp class: #{timestamp_class_name.inspect}"
84
90
  end
85
91
  when OPEN_STRUCT_KEY
86
92
  ostruct_values = load_value(value[OPEN_STRUCT_KEY])
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.6.7"
3
+ VERSION = "2.6.9"
4
4
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.7
4
+ version: 2.6.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-07-28 00:00:00.000000000 Z
10
+ date: 2026-08-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64