graphql 1.10.4 → 1.10.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- context.schema.cursor_encoder.encode(idx.to_s)
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
- @before_value
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
- @after_value
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
- context.schema.cursor_encoder.encode(offset.to_s)
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 && (relation_limit(paginated_nodes).nil? || relation_limit(paginated_nodes) > first) && last.nil?
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
- @problems.nil?
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
- @problems.push({ "path" => path || [], "explanation" => explanation })
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.each do |p|
20
- item_path = [path, *p["path"]]
21
- add_problem(p["explanation"], item_path)
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::CoercionError, GraphQL::ExecutionError => ex
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
- return @first if defined? @first
38
-
39
- @first = get_limited_arg(:first)
40
- @first = max_page_size if @first && max_page_size && @first > max_page_size
41
- @first
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
- return @last if defined? @last
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 ||= get_limited_arg(: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 ||= get_limited_arg(: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
- # Return a sanitized `arguments[arg_name]` (don't allow negatives)
156
- def get_limited_arg(arg_name)
157
- arg_value = arguments[arg_name]
158
- if arg_value.nil?
159
- arg_value
160
- elsif arg_value < 0
161
- 0
162
- else
163
- arg_value
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
- return @first if defined? @first
54
-
55
- @first = get_limited_arg(:first)
56
- @first = max_page_size if @first && max_page_size && @first > max_page_size
57
- @first
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
- return @last if defined? @last
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
@@ -67,8 +67,21 @@ module GraphQL
67
67
 
68
68
  def validate_non_null_input(value, ctx)
69
69
  result = Query::InputValidationResult.new
70
- if value.is_a?(GraphQL::Language::Nodes::Enum) || coerce_non_null_input(value, ctx).nil?
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
@@ -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
@@ -140,6 +140,9 @@ module GraphQL
140
140
  use(plugin)
141
141
  end
142
142
  end
143
+
144
+ # Empty `orphan_types` -- this will make unreachable types ... unreachable.
145
+ own_orphan_types.clear
143
146
  end
144
147
  end
145
148
 
@@ -43,13 +43,21 @@ module GraphQL
43
43
 
44
44
  def validate_non_null_input(value, ctx)
45
45
  result = Query::InputValidationResult.new
46
- if coerce_input(value, ctx).nil?
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
- false
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
- !type.kind.non_null?
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
- (!ast_value.nil?)
24
- end && validate(ast_value, type.of_type)
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).all? { |val| validate(val, item_type) }
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
- true
42
+ @valid_response
30
43
  elsif type.kind.scalar? && constant_scalar?(ast_value)
31
44
  maybe_raise_if_invalid(ast_value) do
32
- type.valid_input?(ast_value, @context)
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.valid_input?(ast_value.name, @context)
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
- false
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
- required_input_fields_are_present(type, ast_value) && present_input_field_values_are_valid(type, ast_value)
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
- false
65
+ @invalid_response
50
66
  end
51
67
  end
52
68
  end
53
69
 
54
- private
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
- e = LiteralValidationError.new
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.all? do |name|
93
- validate(GraphQL::Language::Nodes::NullValue.new(name: name), @warden.arguments(type).find { |f| f.name == name }.type )
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.all? do |value|
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
- validate(value.value, type)
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