graphql 1.10.4 → 1.10.5
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 +4 -4
- data/lib/graphql.rb +0 -1
- data/lib/graphql/analysis/ast/query_complexity.rb +117 -105
- data/lib/graphql/execution/lookahead.rb +21 -108
- data/lib/graphql/pagination/active_record_relation_connection.rb +7 -1
- data/lib/graphql/pagination/array_connection.rb +2 -2
- data/lib/graphql/pagination/connection.rb +17 -5
- data/lib/graphql/pagination/relation_connection.rb +3 -2
- data/lib/graphql/query/input_validation_result.rb +23 -6
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/relay/array_connection.rb +8 -10
- data/lib/graphql/relay/base_connection.rb +19 -11
- data/lib/graphql/relay/relation_connection.rb +8 -10
- data/lib/graphql/scalar_type.rb +14 -1
- data/lib/graphql/schema.rb +6 -0
- data/lib/graphql/schema/build_from_definition.rb +3 -0
- data/lib/graphql/schema/scalar.rb +9 -1
- data/lib/graphql/static_validation/literal_validator.rb +47 -22
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +42 -87
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +17 -5
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +25 -20
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +9 -9
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- metadata +2 -3
- data/lib/graphql/literal_validation_error.rb +0 -6
@@ -6,12 +6,18 @@ module GraphQL
|
|
6
6
|
# Customizes `RelationConnection` to work with `ActiveRecord::Relation`s.
|
7
7
|
class ActiveRecordRelationConnection < Pagination::RelationConnection
|
8
8
|
def relation_count(relation)
|
9
|
-
if relation.respond_to?(:unscope)
|
9
|
+
int_or_hash = if relation.respond_to?(:unscope)
|
10
10
|
relation.unscope(:order).count(:all)
|
11
11
|
else
|
12
12
|
# Rails 3
|
13
13
|
relation.count
|
14
14
|
end
|
15
|
+
if int_or_hash.is_a?(Integer)
|
16
|
+
int_or_hash
|
17
|
+
else
|
18
|
+
# Grouped relations return count-by-group hashes
|
19
|
+
int_or_hash.length
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
23
|
def relation_limit(relation)
|
@@ -21,7 +21,7 @@ module GraphQL
|
|
21
21
|
|
22
22
|
def cursor_for(item)
|
23
23
|
idx = items.find_index(item) + 1
|
24
|
-
|
24
|
+
encode(idx.to_s)
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
@@ -59,7 +59,7 @@ module GraphQL
|
|
59
59
|
sliced_nodes.count > first
|
60
60
|
elsif before
|
61
61
|
# The original array is longer than the `before` index
|
62
|
-
index_from_cursor(before) < items.length
|
62
|
+
index_from_cursor(before) < items.length + 1
|
63
63
|
else
|
64
64
|
false
|
65
65
|
end
|
@@ -29,14 +29,22 @@ module GraphQL
|
|
29
29
|
# Raw access to client-provided values. (`max_page_size` not applied to first or last.)
|
30
30
|
attr_accessor :before_value, :after_value, :first_value, :last_value
|
31
31
|
|
32
|
-
# @return [String, nil] the client-provided cursor
|
32
|
+
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
|
33
33
|
def before
|
34
|
-
@
|
34
|
+
if defined?(@before)
|
35
|
+
@before
|
36
|
+
else
|
37
|
+
@before = @before_value == "" ? nil : @before_value
|
38
|
+
end
|
35
39
|
end
|
36
40
|
|
37
|
-
# @return [String, nil] the client-provided cursor
|
41
|
+
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
|
38
42
|
def after
|
39
|
-
@
|
43
|
+
if defined?(@after)
|
44
|
+
@after
|
45
|
+
else
|
46
|
+
@after = @after_value == "" ? nil : @after_value
|
47
|
+
end
|
40
48
|
end
|
41
49
|
|
42
50
|
# @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation`
|
@@ -147,7 +155,11 @@ module GraphQL
|
|
147
155
|
end
|
148
156
|
|
149
157
|
def decode(cursor)
|
150
|
-
context.schema.cursor_encoder.decode(cursor)
|
158
|
+
context.schema.cursor_encoder.decode(cursor, nonce: true)
|
159
|
+
end
|
160
|
+
|
161
|
+
def encode(cursor)
|
162
|
+
context.schema.cursor_encoder.encode(cursor, nonce: true)
|
151
163
|
end
|
152
164
|
|
153
165
|
# A wrapper around paginated items. It includes a {cursor} for pagination
|
@@ -44,7 +44,7 @@ module GraphQL
|
|
44
44
|
load_nodes
|
45
45
|
# index in nodes + existing offset + 1 (because it's offset, not index)
|
46
46
|
offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) + (relation_offset(items) || 0)
|
47
|
-
|
47
|
+
encode(offset.to_s)
|
48
48
|
end
|
49
49
|
|
50
50
|
private
|
@@ -137,8 +137,9 @@ module GraphQL
|
|
137
137
|
def limited_nodes
|
138
138
|
@limited_nodes ||= begin
|
139
139
|
paginated_nodes = sliced_nodes
|
140
|
+
previous_limit = relation_limit(paginated_nodes)
|
140
141
|
|
141
|
-
if first && (
|
142
|
+
if first && (previous_limit.nil? || previous_limit > first)
|
142
143
|
# `first` would create a stricter limit that the one already applied, so add it
|
143
144
|
paginated_nodes = set_limit(paginated_nodes, first)
|
144
145
|
end
|
@@ -4,22 +4,39 @@ module GraphQL
|
|
4
4
|
class InputValidationResult
|
5
5
|
attr_accessor :problems
|
6
6
|
|
7
|
+
def initialize(valid: true, problems: nil)
|
8
|
+
@valid = valid
|
9
|
+
@problems = problems
|
10
|
+
end
|
11
|
+
|
7
12
|
def valid?
|
8
|
-
@
|
13
|
+
@valid
|
9
14
|
end
|
10
15
|
|
11
|
-
def add_problem(explanation, path = nil)
|
16
|
+
def add_problem(explanation, path = nil, extensions: nil, message: nil)
|
12
17
|
@problems ||= []
|
13
|
-
@
|
18
|
+
@valid = false
|
19
|
+
problem = { "path" => path || [], "explanation" => explanation }
|
20
|
+
if extensions
|
21
|
+
problem["extensions"] = extensions
|
22
|
+
end
|
23
|
+
if message
|
24
|
+
problem["message"] = message
|
25
|
+
end
|
26
|
+
@problems.push(problem)
|
14
27
|
end
|
15
28
|
|
16
29
|
def merge_result!(path, inner_result)
|
17
30
|
return if inner_result.valid?
|
18
31
|
|
19
|
-
inner_result.problems
|
20
|
-
|
21
|
-
|
32
|
+
if inner_result.problems
|
33
|
+
inner_result.problems.each do |p|
|
34
|
+
item_path = [path, *p["path"]]
|
35
|
+
add_problem(p["explanation"], item_path, message: p["message"], extensions: p["extensions"])
|
36
|
+
end
|
22
37
|
end
|
38
|
+
# It could have been explicitly set on inner_result (if it had no problems)
|
39
|
+
@valid = false
|
23
40
|
end
|
24
41
|
end
|
25
42
|
end
|
@@ -52,7 +52,7 @@ module GraphQL
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
55
|
-
rescue GraphQL::
|
55
|
+
rescue GraphQL::ExecutionError => ex
|
56
56
|
# TODO: This should really include the path to the problematic node in the variable value
|
57
57
|
# like InputValidationResults generated by validate_non_null_input but unfortunately we don't
|
58
58
|
# have this information available in the coerce_input call chain. Note this path is the path
|
@@ -34,19 +34,17 @@ module GraphQL
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def first
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
37
|
+
@first ||= begin
|
38
|
+
capped = limit_pagination_argument(arguments[:first], max_page_size)
|
39
|
+
if capped.nil? && last.nil?
|
40
|
+
capped = max_page_size
|
41
|
+
end
|
42
|
+
capped
|
43
|
+
end
|
42
44
|
end
|
43
45
|
|
44
46
|
def last
|
45
|
-
|
46
|
-
|
47
|
-
@last = get_limited_arg(:last)
|
48
|
-
@last = max_page_size if @last && max_page_size && @last > max_page_size
|
49
|
-
@last
|
47
|
+
@last ||= limit_pagination_argument(arguments[:last], max_page_size)
|
50
48
|
end
|
51
49
|
|
52
50
|
# apply first / last limit results
|
@@ -81,7 +81,13 @@ module GraphQL
|
|
81
81
|
# The value passed as `first:`, if there was one. Negative numbers become `0`.
|
82
82
|
# @return [Integer, nil]
|
83
83
|
def first
|
84
|
-
@first ||=
|
84
|
+
@first ||= begin
|
85
|
+
capped = limit_pagination_argument(arguments[:first], max_page_size)
|
86
|
+
if capped.nil? && last.nil?
|
87
|
+
capped = max_page_size
|
88
|
+
end
|
89
|
+
capped
|
90
|
+
end
|
85
91
|
end
|
86
92
|
|
87
93
|
# The value passed as `after:`, if there was one
|
@@ -93,7 +99,7 @@ module GraphQL
|
|
93
99
|
# The value passed as `last:`, if there was one. Negative numbers become `0`.
|
94
100
|
# @return [Integer, nil]
|
95
101
|
def last
|
96
|
-
@last ||=
|
102
|
+
@last ||= limit_pagination_argument(arguments[:last], max_page_size)
|
97
103
|
end
|
98
104
|
|
99
105
|
# The value passed as `before:`, if there was one
|
@@ -152,16 +158,18 @@ module GraphQL
|
|
152
158
|
|
153
159
|
private
|
154
160
|
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
161
|
+
# @param argument [nil, Integer] `first` or `last`, as provided by the client
|
162
|
+
# @param max_page_size [nil, Integer]
|
163
|
+
# @return [nil, Integer] `nil` if the input was `nil`, otherwise a value between `0` and `max_page_size`
|
164
|
+
def limit_pagination_argument(argument, max_page_size)
|
165
|
+
if argument
|
166
|
+
if argument < 0
|
167
|
+
argument = 0
|
168
|
+
elsif max_page_size && argument > max_page_size
|
169
|
+
argument = max_page_size
|
170
|
+
end
|
164
171
|
end
|
172
|
+
argument
|
165
173
|
end
|
166
174
|
|
167
175
|
def paged_nodes
|
@@ -50,19 +50,17 @@ module GraphQL
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def first
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
@first ||= begin
|
54
|
+
capped = limit_pagination_argument(arguments[:first], max_page_size)
|
55
|
+
if capped.nil? && last.nil?
|
56
|
+
capped = max_page_size
|
57
|
+
end
|
58
|
+
capped
|
59
|
+
end
|
58
60
|
end
|
59
61
|
|
60
62
|
def last
|
61
|
-
|
62
|
-
|
63
|
-
@last = get_limited_arg(:last)
|
64
|
-
@last = max_page_size if @last && max_page_size && @last > max_page_size
|
65
|
-
@last
|
63
|
+
@last ||= limit_pagination_argument(arguments[:last], max_page_size)
|
66
64
|
end
|
67
65
|
|
68
66
|
private
|
data/lib/graphql/scalar_type.rb
CHANGED
@@ -67,8 +67,21 @@ module GraphQL
|
|
67
67
|
|
68
68
|
def validate_non_null_input(value, ctx)
|
69
69
|
result = Query::InputValidationResult.new
|
70
|
-
|
70
|
+
|
71
|
+
coerced_result = begin
|
72
|
+
coerce_non_null_input(value, ctx)
|
73
|
+
rescue GraphQL::CoercionError => err
|
74
|
+
err
|
75
|
+
end
|
76
|
+
|
77
|
+
if value.is_a?(GraphQL::Language::Nodes::Enum) || coerced_result.nil?
|
71
78
|
result.add_problem("Could not coerce value #{GraphQL::Language.serialize(value)} to #{name}")
|
79
|
+
elsif coerced_result.is_a?(GraphQL::CoercionError)
|
80
|
+
result.add_problem(
|
81
|
+
coerced_result.message,
|
82
|
+
message: coerced_result.message,
|
83
|
+
extensions: coerced_result.extensions
|
84
|
+
)
|
72
85
|
end
|
73
86
|
result
|
74
87
|
end
|
data/lib/graphql/schema.rb
CHANGED
@@ -908,6 +908,7 @@ module GraphQL
|
|
908
908
|
schema_defn.cursor_encoder = cursor_encoder
|
909
909
|
schema_defn.tracers.concat(tracers)
|
910
910
|
schema_defn.query_analyzers.concat(query_analyzers)
|
911
|
+
schema_defn.analysis_engine = analysis_engine
|
911
912
|
|
912
913
|
schema_defn.middleware.concat(all_middleware)
|
913
914
|
schema_defn.multiplex_analyzers.concat(multiplex_analyzers)
|
@@ -934,6 +935,11 @@ module GraphQL
|
|
934
935
|
if !schema_defn.interpreter?
|
935
936
|
schema_defn.instrumenters[:query] << GraphQL::Schema::Member::Instrumentation
|
936
937
|
end
|
938
|
+
|
939
|
+
if new_connections?
|
940
|
+
schema_defn.connections = self.connections
|
941
|
+
end
|
942
|
+
|
937
943
|
schema_defn.send(:rebuild_artifacts)
|
938
944
|
|
939
945
|
schema_defn
|
@@ -43,13 +43,21 @@ module GraphQL
|
|
43
43
|
|
44
44
|
def validate_non_null_input(value, ctx)
|
45
45
|
result = Query::InputValidationResult.new
|
46
|
-
|
46
|
+
coerced_result = begin
|
47
|
+
coerce_input(value, ctx)
|
48
|
+
rescue GraphQL::CoercionError => err
|
49
|
+
err
|
50
|
+
end
|
51
|
+
|
52
|
+
if coerced_result.nil?
|
47
53
|
str_value = if value == Float::INFINITY
|
48
54
|
""
|
49
55
|
else
|
50
56
|
" #{GraphQL::Language.serialize(value)}"
|
51
57
|
end
|
52
58
|
result.add_problem("Could not coerce value#{str_value} to #{graphql_name}")
|
59
|
+
elsif coerced_result.is_a?(GraphQL::CoercionError)
|
60
|
+
result.add_problem(coerced_result.message, message: coerced_result.message, extensions: coerced_result.extensions)
|
53
61
|
end
|
54
62
|
result
|
55
63
|
end
|
@@ -6,59 +6,73 @@ module GraphQL
|
|
6
6
|
def initialize(context:)
|
7
7
|
@context = context
|
8
8
|
@warden = context.warden
|
9
|
+
@invalid_response = GraphQL::Query::InputValidationResult.new(valid: false, problems: [])
|
10
|
+
@valid_response = GraphQL::Query::InputValidationResult.new(valid: true, problems: [])
|
9
11
|
end
|
10
12
|
|
11
13
|
def validate(ast_value, type)
|
14
|
+
catch(:invalid) do
|
15
|
+
recursively_validate(ast_value, type)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def recursively_validate(ast_value, type)
|
12
22
|
if type.nil?
|
13
23
|
# this means we're an undefined argument, see #present_input_field_values_are_valid
|
14
24
|
maybe_raise_if_invalid(ast_value) do
|
15
|
-
|
25
|
+
@invalid_response
|
16
26
|
end
|
17
27
|
elsif ast_value.is_a?(GraphQL::Language::Nodes::NullValue)
|
18
28
|
maybe_raise_if_invalid(ast_value) do
|
19
|
-
|
29
|
+
type.kind.non_null? ? @invalid_response : @valid_response
|
20
30
|
end
|
21
31
|
elsif type.kind.non_null?
|
22
32
|
maybe_raise_if_invalid(ast_value) do
|
23
|
-
|
24
|
-
|
33
|
+
ast_value.nil? ?
|
34
|
+
@invalid_response :
|
35
|
+
recursively_validate(ast_value, type.of_type)
|
36
|
+
end
|
25
37
|
elsif type.kind.list?
|
26
38
|
item_type = type.of_type
|
27
|
-
ensure_array(ast_value).
|
39
|
+
results = ensure_array(ast_value).map { |val| recursively_validate(val, item_type) }
|
40
|
+
merge_results(results)
|
28
41
|
elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
29
|
-
|
42
|
+
@valid_response
|
30
43
|
elsif type.kind.scalar? && constant_scalar?(ast_value)
|
31
44
|
maybe_raise_if_invalid(ast_value) do
|
32
|
-
type.
|
45
|
+
type.validate_input(ast_value, @context)
|
33
46
|
end
|
34
47
|
elsif type.kind.enum?
|
35
48
|
maybe_raise_if_invalid(ast_value) do
|
36
49
|
if ast_value.is_a?(GraphQL::Language::Nodes::Enum)
|
37
|
-
type.
|
50
|
+
type.validate_input(ast_value.name, @context)
|
38
51
|
else
|
39
52
|
# if our ast_value isn't an Enum it's going to be invalid so return false
|
40
|
-
|
53
|
+
@invalid_response
|
41
54
|
end
|
42
55
|
end
|
43
56
|
elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
|
44
57
|
maybe_raise_if_invalid(ast_value) do
|
45
|
-
|
58
|
+
merge_results([
|
59
|
+
required_input_fields_are_present(type, ast_value),
|
60
|
+
present_input_field_values_are_valid(type, ast_value)
|
61
|
+
])
|
46
62
|
end
|
47
63
|
else
|
48
64
|
maybe_raise_if_invalid(ast_value) do
|
49
|
-
|
65
|
+
@invalid_response
|
50
66
|
end
|
51
67
|
end
|
52
68
|
end
|
53
69
|
|
54
|
-
|
55
|
-
|
70
|
+
# When `error_bubbling` is false, we want to bail on the first failure that we find.
|
71
|
+
# Use `throw` to escape the current call stack, returning the invalid response.
|
56
72
|
def maybe_raise_if_invalid(ast_value)
|
57
73
|
ret = yield
|
58
|
-
if !@context.schema.error_bubbling && !ret
|
59
|
-
|
60
|
-
e.ast_value = ast_value
|
61
|
-
raise e
|
74
|
+
if !@context.schema.error_bubbling && !ret.valid?
|
75
|
+
throw(:invalid, ret)
|
62
76
|
else
|
63
77
|
ret
|
64
78
|
end
|
@@ -87,28 +101,39 @@ module GraphQL
|
|
87
101
|
present_field_names = ast_node.arguments.map(&:name)
|
88
102
|
missing_required_field_names = required_field_names - present_field_names
|
89
103
|
if @context.schema.error_bubbling
|
90
|
-
missing_required_field_names.empty?
|
104
|
+
missing_required_field_names.empty? ? @valid_response : @invalid_response
|
91
105
|
else
|
92
|
-
missing_required_field_names.
|
93
|
-
|
106
|
+
results = missing_required_field_names.map do |name|
|
107
|
+
arg_type = @warden.arguments(type).find { |f| f.name == name }.type
|
108
|
+
recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
|
94
109
|
end
|
110
|
+
merge_results(results)
|
95
111
|
end
|
96
112
|
end
|
97
113
|
|
98
114
|
def present_input_field_values_are_valid(type, ast_node)
|
99
115
|
field_map = @warden.arguments(type).reduce({}) { |m, f| m[f.name] = f; m}
|
100
|
-
ast_node.arguments.
|
116
|
+
results = ast_node.arguments.map do |value|
|
101
117
|
field = field_map[value.name]
|
102
118
|
# we want to call validate on an argument even if it's an invalid one
|
103
119
|
# so that our raise exception is on it instead of the entire InputObject
|
104
120
|
type = field && field.type
|
105
|
-
|
121
|
+
recursively_validate(value.value, type)
|
106
122
|
end
|
123
|
+
merge_results(results)
|
107
124
|
end
|
108
125
|
|
109
126
|
def ensure_array(value)
|
110
127
|
value.is_a?(Array) ? value : [value]
|
111
128
|
end
|
129
|
+
|
130
|
+
def merge_results(results_list)
|
131
|
+
merged_result = Query::InputValidationResult.new
|
132
|
+
results_list.each do |inner_result|
|
133
|
+
merged_result.merge_result!([], inner_result)
|
134
|
+
end
|
135
|
+
merged_result
|
136
|
+
end
|
112
137
|
end
|
113
138
|
end
|
114
139
|
end
|