graphql 2.3.0 → 2.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphql/templates/schema.erb +3 -0
- data/lib/graphql/analysis/ast/field_usage.rb +7 -5
- data/lib/graphql/execution/interpreter/argument_value.rb +5 -1
- data/lib/graphql/language/lexer.rb +19 -2
- data/lib/graphql/language/parser.rb +14 -4
- data/lib/graphql/language.rb +23 -0
- data/lib/graphql/query.rb +1 -1
- data/lib/graphql/schema/argument.rb +1 -0
- data/lib/graphql/schema/build_from_definition.rb +6 -0
- data/lib/graphql/schema/input_object.rb +1 -2
- data/lib/graphql/schema/mutation.rb +7 -0
- data/lib/graphql/schema/resolver.rb +10 -5
- data/lib/graphql/schema.rb +11 -0
- data/lib/graphql/testing/helpers.rb +24 -2
- data/lib/graphql/tracing/prometheus_trace.rb +2 -2
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +9 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 566d6d5c49b331b3f38e2f4d338c635bae02a86b14a004ff55f1eccccb973181
|
4
|
+
data.tar.gz: 0a8a048f8644e933252ca30459386e2049ac8c3093731709d344bf898f884731
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12136553e963ed98012887215d1f21efd31c2a3bacd40ada6836ca3884680cb4ac6a8c7a1fabaa7fc8699c0ba6db9f20418ae068a74694daef8db1d13e506933
|
7
|
+
data.tar.gz: 1faab9e89cf122660fb277df551ba026685cefeaa2fba9d83b590a5a18d4c4dc1202712ecf404c2998eb12c27a1219bd548944ef5f65daaad79371163ca66b43
|
@@ -26,6 +26,9 @@ class <%= schema_name %> < GraphQL::Schema
|
|
26
26
|
raise(GraphQL::RequiredImplementationMissingError)
|
27
27
|
end
|
28
28
|
|
29
|
+
# Limit the size of incoming queries:
|
30
|
+
max_query_string_tokens(5000)
|
31
|
+
|
29
32
|
# Stop validating when it encounters this many errors:
|
30
33
|
validate_max_errors(100)
|
31
34
|
end
|
@@ -41,7 +41,9 @@ module GraphQL
|
|
41
41
|
@used_deprecated_arguments << argument.definition.path
|
42
42
|
end
|
43
43
|
|
44
|
-
|
44
|
+
arg_val = argument.value
|
45
|
+
|
46
|
+
next if arg_val.nil?
|
45
47
|
|
46
48
|
argument_type = argument.definition.type
|
47
49
|
if argument_type.non_null?
|
@@ -49,18 +51,18 @@ module GraphQL
|
|
49
51
|
end
|
50
52
|
|
51
53
|
if argument_type.kind.input_object?
|
52
|
-
extract_deprecated_arguments(argument.
|
54
|
+
extract_deprecated_arguments(argument.original_value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
|
53
55
|
elsif argument_type.kind.enum?
|
54
|
-
extract_deprecated_enum_value(argument_type,
|
56
|
+
extract_deprecated_enum_value(argument_type, arg_val)
|
55
57
|
elsif argument_type.list?
|
56
58
|
inner_type = argument_type.unwrap
|
57
59
|
case inner_type.kind
|
58
60
|
when TypeKinds::INPUT_OBJECT
|
59
|
-
argument.
|
61
|
+
argument.original_value.each do |value|
|
60
62
|
extract_deprecated_arguments(value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
|
61
63
|
end
|
62
64
|
when TypeKinds::ENUM
|
63
|
-
|
65
|
+
arg_val.each do |value|
|
64
66
|
extract_deprecated_enum_value(inner_type, value)
|
65
67
|
end
|
66
68
|
else
|
@@ -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
|
|
@@ -3,7 +3,7 @@ module GraphQL
|
|
3
3
|
module Language
|
4
4
|
|
5
5
|
class Lexer
|
6
|
-
def initialize(graphql_str, filename: nil)
|
6
|
+
def initialize(graphql_str, filename: nil, max_tokens: nil)
|
7
7
|
if !(graphql_str.encoding == Encoding::UTF_8 || graphql_str.ascii_only?)
|
8
8
|
graphql_str = graphql_str.dup.force_encoding(Encoding::UTF_8)
|
9
9
|
end
|
@@ -11,6 +11,8 @@ module GraphQL
|
|
11
11
|
@filename = filename
|
12
12
|
@scanner = StringScanner.new(graphql_str)
|
13
13
|
@pos = nil
|
14
|
+
@max_tokens = max_tokens || Float::INFINITY
|
15
|
+
@tokens_count = 0
|
14
16
|
end
|
15
17
|
|
16
18
|
def eos?
|
@@ -22,6 +24,10 @@ module GraphQL
|
|
22
24
|
def advance
|
23
25
|
@scanner.skip(IGNORE_REGEXP)
|
24
26
|
return false if @scanner.eos?
|
27
|
+
@tokens_count += 1
|
28
|
+
if @tokens_count > @max_tokens
|
29
|
+
raise_parse_error("This query is too large to execute.")
|
30
|
+
end
|
25
31
|
@pos = @scanner.pos
|
26
32
|
next_byte = @string.getbyte(@pos)
|
27
33
|
next_byte_is_for = FIRST_BYTES[next_byte]
|
@@ -52,6 +58,17 @@ module GraphQL
|
|
52
58
|
:IDENTIFIER
|
53
59
|
when ByteFor::NUMBER
|
54
60
|
@scanner.skip(NUMERIC_REGEXP)
|
61
|
+
|
62
|
+
if GraphQL.reject_numbers_followed_by_names
|
63
|
+
new_pos = @scanner.pos
|
64
|
+
peek_byte = @string.getbyte(new_pos)
|
65
|
+
next_first_byte = FIRST_BYTES[peek_byte]
|
66
|
+
if next_first_byte == ByteFor::NAME || next_first_byte == ByteFor::IDENTIFIER
|
67
|
+
number_part = token_value
|
68
|
+
name_part = @scanner.scan(IDENTIFIER_REGEXP)
|
69
|
+
raise_parse_error("Name after number is not allowed (in `#{number_part}#{name_part}`)")
|
70
|
+
end
|
71
|
+
end
|
55
72
|
# Check for a matched decimal:
|
56
73
|
@scanner[1] ? :FLOAT : :INT
|
57
74
|
when ByteFor::ELLIPSIS
|
@@ -156,6 +173,7 @@ module GraphQL
|
|
156
173
|
INT_REGEXP = /-?(?:[0]|[1-9][0-9]*)/
|
157
174
|
FLOAT_DECIMAL_REGEXP = /[.][0-9]+/
|
158
175
|
FLOAT_EXP_REGEXP = /[eE][+-]?[0-9]+/
|
176
|
+
# TODO: FLOAT_EXP_REGEXP should not be allowed to follow INT_REGEXP, integers are not allowed to have exponent parts.
|
159
177
|
NUMERIC_REGEXP = /#{INT_REGEXP}(#{FLOAT_DECIMAL_REGEXP}#{FLOAT_EXP_REGEXP}|#{FLOAT_DECIMAL_REGEXP}|#{FLOAT_EXP_REGEXP})?/
|
160
178
|
|
161
179
|
KEYWORDS = [
|
@@ -250,7 +268,6 @@ module GraphQL
|
|
250
268
|
FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/
|
251
269
|
N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}{4,}#{Punctuation::RCURLY}}x
|
252
270
|
UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})}
|
253
|
-
# # https://graphql.github.io/graphql-spec/June2018/#sec-String-Value
|
254
271
|
STRING_ESCAPE = %r{[\\][\\/bfnrt]}
|
255
272
|
BLOCK_QUOTE = '"""'
|
256
273
|
ESCAPED_QUOTE = /\\"/;
|
@@ -12,8 +12,8 @@ module GraphQL
|
|
12
12
|
class << self
|
13
13
|
attr_accessor :cache
|
14
14
|
|
15
|
-
def parse(graphql_str, filename: nil, trace: Tracing::NullTrace)
|
16
|
-
self.new(graphql_str, filename: filename, trace: trace).parse
|
15
|
+
def parse(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil)
|
16
|
+
self.new(graphql_str, filename: filename, trace: trace, max_tokens: max_tokens).parse
|
17
17
|
end
|
18
18
|
|
19
19
|
def parse_file(filename, trace: Tracing::NullTrace)
|
@@ -27,14 +27,15 @@ module GraphQL
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace)
|
30
|
+
def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil)
|
31
31
|
if graphql_str.nil?
|
32
32
|
raise GraphQL::ParseError.new("No query string was present", nil, nil, nil)
|
33
33
|
end
|
34
|
-
@lexer = Lexer.new(graphql_str, filename: filename)
|
34
|
+
@lexer = Lexer.new(graphql_str, filename: filename, max_tokens: max_tokens)
|
35
35
|
@graphql_str = graphql_str
|
36
36
|
@filename = filename
|
37
37
|
@trace = trace
|
38
|
+
@dedup_identifiers = false
|
38
39
|
end
|
39
40
|
|
40
41
|
def parse
|
@@ -732,6 +733,9 @@ module GraphQL
|
|
732
733
|
# Only use when we care about the expected token's value
|
733
734
|
def expect_token_value(tok)
|
734
735
|
token_value = @lexer.token_value
|
736
|
+
if @dedup_identifiers
|
737
|
+
token_value = -token_value
|
738
|
+
end
|
735
739
|
expect_token(tok)
|
736
740
|
token_value
|
737
741
|
end
|
@@ -741,6 +745,12 @@ module GraphQL
|
|
741
745
|
def debug_token_value
|
742
746
|
@lexer.debug_token_value(token_name)
|
743
747
|
end
|
748
|
+
class SchemaParser < Parser
|
749
|
+
def initialize(*args, **kwargs)
|
750
|
+
super
|
751
|
+
@dedup_identifiers = true
|
752
|
+
end
|
753
|
+
end
|
744
754
|
end
|
745
755
|
end
|
746
756
|
end
|
data/lib/graphql/language.rb
CHANGED
@@ -33,6 +33,12 @@ module GraphQL
|
|
33
33
|
else
|
34
34
|
JSON.generate(value, quirks_mode: true)
|
35
35
|
end
|
36
|
+
rescue JSON::GeneratorError
|
37
|
+
if Float::INFINITY == value
|
38
|
+
"Infinity"
|
39
|
+
else
|
40
|
+
raise
|
41
|
+
end
|
36
42
|
end
|
37
43
|
|
38
44
|
# Returns a new string if any single-quoted newlines were escaped.
|
@@ -70,5 +76,22 @@ module GraphQL
|
|
70
76
|
end
|
71
77
|
new_query_str || query_str
|
72
78
|
end
|
79
|
+
|
80
|
+
INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP = %r{
|
81
|
+
(
|
82
|
+
((?<num>#{Lexer::INT_REGEXP}(#{Lexer::FLOAT_EXP_REGEXP})?)(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
|
83
|
+
|
|
84
|
+
((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP}#{Lexer::FLOAT_EXP_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
|
85
|
+
|
|
86
|
+
((?<num>#{Lexer::INT_REGEXP}#{Lexer::FLOAT_DECIMAL_REGEXP})(?<name>#{Lexer::IDENTIFIER_REGEXP})#{Lexer::IGNORE_REGEXP}:)
|
87
|
+
)}x
|
88
|
+
|
89
|
+
def self.add_space_between_numbers_and_names(query_str)
|
90
|
+
if query_str.match?(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP)
|
91
|
+
query_str.gsub(INVALID_NUMBER_FOLLOWED_BY_NAME_REGEXP, "\\k<num> \\k<name>:")
|
92
|
+
else
|
93
|
+
query_str
|
94
|
+
end
|
95
|
+
end
|
73
96
|
end
|
74
97
|
end
|
data/lib/graphql/query.rb
CHANGED
@@ -395,7 +395,7 @@ module GraphQL
|
|
395
395
|
parse_error = nil
|
396
396
|
@document ||= begin
|
397
397
|
if query_string
|
398
|
-
GraphQL.parse(query_string, trace: self.current_trace)
|
398
|
+
GraphQL.parse(query_string, trace: self.current_trace, max_tokens: @schema.max_query_string_tokens)
|
399
399
|
end
|
400
400
|
rescue GraphQL::ParseError => err
|
401
401
|
parse_error = err
|
@@ -290,6 +290,7 @@ module GraphQL
|
|
290
290
|
# TODO code smell to access such a deeply-nested constant in a distant module
|
291
291
|
argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
|
292
292
|
value: resolved_loaded_value,
|
293
|
+
original_value: resolved_coerced_value,
|
293
294
|
definition: self,
|
294
295
|
default_used: default_used,
|
295
296
|
)
|
@@ -7,10 +7,16 @@ module GraphQL
|
|
7
7
|
class << self
|
8
8
|
# @see {Schema.from_definition}
|
9
9
|
def from_definition(schema_superclass, definition_string, parser: GraphQL.default_parser, **kwargs)
|
10
|
+
if defined?(parser::SchemaParser)
|
11
|
+
parser = parser::SchemaParser
|
12
|
+
end
|
10
13
|
from_document(schema_superclass, parser.parse(definition_string), **kwargs)
|
11
14
|
end
|
12
15
|
|
13
16
|
def from_definition_path(schema_superclass, definition_path, parser: GraphQL.default_parser, **kwargs)
|
17
|
+
if defined?(parser::SchemaParser)
|
18
|
+
parser = parser::SchemaParser
|
19
|
+
end
|
14
20
|
from_document(schema_superclass, parser.parse_file(definition_path), **kwargs)
|
15
21
|
end
|
16
22
|
|
@@ -215,8 +215,7 @@ module GraphQL
|
|
215
215
|
if resolved_arguments.is_a?(GraphQL::Error)
|
216
216
|
raise resolved_arguments
|
217
217
|
else
|
218
|
-
|
219
|
-
input_obj_instance.prepare
|
218
|
+
self.new(resolved_arguments, ruby_kwargs: resolved_arguments.keyword_arguments, context: ctx, defaults_used: nil)
|
220
219
|
end
|
221
220
|
end
|
222
221
|
end
|
@@ -62,6 +62,13 @@ module GraphQL
|
|
62
62
|
extend GraphQL::Schema::Member::HasFields
|
63
63
|
extend GraphQL::Schema::Resolver::HasPayloadType
|
64
64
|
|
65
|
+
# @api private
|
66
|
+
def call_resolve(_args_hash)
|
67
|
+
# Clear any cached values from `loads` or authorization:
|
68
|
+
dataloader.clear_cache
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
65
72
|
class << self
|
66
73
|
def visible?(context)
|
67
74
|
true
|
@@ -103,11 +103,7 @@ module GraphQL
|
|
103
103
|
end
|
104
104
|
elsif authorized_val
|
105
105
|
# Finally, all the hooks have passed, so resolve it
|
106
|
-
|
107
|
-
public_send(self.class.resolve_method, **loaded_args)
|
108
|
-
else
|
109
|
-
public_send(self.class.resolve_method)
|
110
|
-
end
|
106
|
+
call_resolve(loaded_args)
|
111
107
|
else
|
112
108
|
raise GraphQL::UnauthorizedFieldError.new(context: context, object: object, type: field.owner, field: field)
|
113
109
|
end
|
@@ -117,6 +113,15 @@ module GraphQL
|
|
117
113
|
end
|
118
114
|
end
|
119
115
|
|
116
|
+
# @api private {GraphQL::Schema::Mutation} uses this to clear the dataloader cache
|
117
|
+
def call_resolve(args_hash)
|
118
|
+
if args_hash.any?
|
119
|
+
public_send(self.class.resolve_method, **args_hash)
|
120
|
+
else
|
121
|
+
public_send(self.class.resolve_method)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
120
125
|
# Do the work. Everything happens here.
|
121
126
|
# @return [Object] An object corresponding to the return type
|
122
127
|
def resolve(**args)
|
data/lib/graphql/schema.rb
CHANGED
@@ -643,6 +643,17 @@ module GraphQL
|
|
643
643
|
end
|
644
644
|
end
|
645
645
|
|
646
|
+
# A limit on the number of tokens to accept on incoming query strings.
|
647
|
+
# Use this to prevent parsing maliciously-large query strings.
|
648
|
+
# @return [nil, Integer]
|
649
|
+
def max_query_string_tokens(new_max_tokens = NOT_CONFIGURED)
|
650
|
+
if NOT_CONFIGURED.equal?(new_max_tokens)
|
651
|
+
defined?(@max_query_string_tokens) ? @max_query_string_tokens : find_inherited_value(:max_query_string_tokens)
|
652
|
+
else
|
653
|
+
@max_query_string_tokens = new_max_tokens
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
646
657
|
def default_page_size(new_default_page_size = nil)
|
647
658
|
if new_default_page_size
|
648
659
|
@default_page_size = new_default_page_size
|
@@ -39,9 +39,9 @@ module GraphQL
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
def run_graphql_field(schema, field_path, object, arguments: {}, context: {})
|
42
|
+
def run_graphql_field(schema, field_path, object, arguments: {}, context: {}, ast_node: nil, lookahead: nil)
|
43
43
|
type_name, *field_names = field_path.split(".")
|
44
|
-
dummy_query = GraphQL::Query.new(schema, context: context)
|
44
|
+
dummy_query = GraphQL::Query.new(schema, "{ __typename }", context: context)
|
45
45
|
query_context = dummy_query.context
|
46
46
|
object_type = dummy_query.get_type(type_name) # rubocop:disable Development/ContextIsPassedCop
|
47
47
|
if object_type
|
@@ -57,6 +57,28 @@ module GraphQL
|
|
57
57
|
dummy_query.context.dataloader.run_isolated {
|
58
58
|
field_args = visible_field.coerce_arguments(graphql_result, arguments, query_context)
|
59
59
|
field_args = schema.sync_lazy(field_args)
|
60
|
+
if visible_field.extras.any?
|
61
|
+
extra_args = {}
|
62
|
+
visible_field.extras.each do |extra|
|
63
|
+
extra_args[extra] = case extra
|
64
|
+
when :ast_node
|
65
|
+
ast_node ||= GraphQL::Language::Nodes::Field.new(name: visible_field.graphql_name)
|
66
|
+
when :lookahead
|
67
|
+
lookahead ||= begin
|
68
|
+
ast_node ||= GraphQL::Language::Nodes::Field.new(name: visible_field.graphql_name)
|
69
|
+
Execution::Lookahead.new(
|
70
|
+
query: dummy_query,
|
71
|
+
ast_nodes: [ast_node],
|
72
|
+
field: visible_field,
|
73
|
+
)
|
74
|
+
end
|
75
|
+
else
|
76
|
+
raise ArgumentError, "This extra isn't supported in `run_graphql_field` yet: `#{extra.inspect}`. Open an issue on GitHub to request it: https://github.com/rmosolgo/graphql-ruby/issues/new"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
field_args = field_args.merge_extras(extra_args)
|
81
|
+
end
|
60
82
|
graphql_result = visible_field.resolve(graphql_result, field_args.keyword_arguments, query_context)
|
61
83
|
graphql_result = schema.sync_lazy(graphql_result)
|
62
84
|
}
|
@@ -24,8 +24,8 @@ module GraphQL
|
|
24
24
|
'execute_query_lazy' => "graphql.execute",
|
25
25
|
}.each do |trace_method, platform_key|
|
26
26
|
module_eval <<-RUBY, __FILE__, __LINE__
|
27
|
-
def #{trace_method}(**data
|
28
|
-
instrument_execution("#{platform_key}", "#{trace_method}"
|
27
|
+
def #{trace_method}(**data)
|
28
|
+
instrument_execution("#{platform_key}", "#{trace_method}") { super }
|
29
29
|
end
|
30
30
|
RUBY
|
31
31
|
end
|
data/lib/graphql/version.rb
CHANGED
data/lib/graphql.rb
CHANGED
@@ -42,8 +42,8 @@ This is probably a bug in GraphQL-Ruby, please report this error on GitHub: http
|
|
42
42
|
# Turn a query string or schema definition into an AST
|
43
43
|
# @param graphql_string [String] a GraphQL query string or schema definition
|
44
44
|
# @return [GraphQL::Language::Nodes::Document]
|
45
|
-
def self.parse(graphql_string, trace: GraphQL::Tracing::NullTrace, filename: nil)
|
46
|
-
default_parser.parse(graphql_string, trace: trace, filename: filename)
|
45
|
+
def self.parse(graphql_string, trace: GraphQL::Tracing::NullTrace, filename: nil, max_tokens: nil)
|
46
|
+
default_parser.parse(graphql_string, trace: trace, filename: filename, max_tokens: max_tokens)
|
47
47
|
end
|
48
48
|
|
49
49
|
# Read the contents of `filename` and parse them as GraphQL
|
@@ -74,6 +74,13 @@ This is probably a bug in GraphQL-Ruby, please report this error on GitHub: http
|
|
74
74
|
EMPTY_HASH = {}.freeze
|
75
75
|
EMPTY_ARRAY = [].freeze
|
76
76
|
end
|
77
|
+
|
78
|
+
class << self
|
79
|
+
# If true, the parser should raise when an integer or float is followed immediately by an identifier (instead of a space or punctuation)
|
80
|
+
attr_accessor :reject_numbers_followed_by_names
|
81
|
+
end
|
82
|
+
|
83
|
+
self.reject_numbers_followed_by_names = false
|
77
84
|
end
|
78
85
|
|
79
86
|
# Order matters for these:
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.3.
|
4
|
+
version: 2.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base64
|