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.
@@ -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