graphql 1.12.6 → 1.12.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +1 -1
  3. data/lib/generators/graphql/templates/graphql_controller.erb +2 -2
  4. data/lib/graphql.rb +10 -10
  5. data/lib/graphql/backtrace/table.rb +14 -2
  6. data/lib/graphql/dataloader.rb +44 -15
  7. data/lib/graphql/execution/errors.rb +109 -11
  8. data/lib/graphql/execution/execute.rb +1 -1
  9. data/lib/graphql/execution/interpreter.rb +4 -8
  10. data/lib/graphql/execution/interpreter/runtime.rb +207 -188
  11. data/lib/graphql/introspection.rb +1 -1
  12. data/lib/graphql/introspection/directive_type.rb +7 -3
  13. data/lib/graphql/introspection/schema_type.rb +1 -1
  14. data/lib/graphql/pagination/active_record_relation_connection.rb +7 -0
  15. data/lib/graphql/pagination/connections.rb +1 -1
  16. data/lib/graphql/pagination/relation_connection.rb +8 -1
  17. data/lib/graphql/query.rb +1 -3
  18. data/lib/graphql/query/null_context.rb +7 -1
  19. data/lib/graphql/query/validation_pipeline.rb +1 -1
  20. data/lib/graphql/rake_task.rb +3 -0
  21. data/lib/graphql/schema.rb +49 -237
  22. data/lib/graphql/schema/addition.rb +238 -0
  23. data/lib/graphql/schema/argument.rb +55 -36
  24. data/lib/graphql/schema/directive/transform.rb +13 -1
  25. data/lib/graphql/schema/input_object.rb +2 -2
  26. data/lib/graphql/schema/loader.rb +8 -0
  27. data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
  28. data/lib/graphql/schema/object.rb +19 -5
  29. data/lib/graphql/schema/resolver.rb +46 -24
  30. data/lib/graphql/schema/scalar.rb +3 -1
  31. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +3 -1
  32. data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +6 -2
  33. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  34. data/lib/graphql/static_validation/rules/arguments_are_defined_error.rb +4 -2
  35. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
  36. data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
  37. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  38. data/lib/graphql/static_validation/validator.rb +5 -0
  39. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  40. data/lib/graphql/subscriptions/broadcast_analyzer.rb +0 -3
  41. data/lib/graphql/subscriptions/serialize.rb +11 -1
  42. data/lib/graphql/types/relay/base_connection.rb +4 -0
  43. data/lib/graphql/types/relay/connection_behaviors.rb +21 -10
  44. data/lib/graphql/types/relay/edge_behaviors.rb +12 -1
  45. data/lib/graphql/version.rb +1 -1
  46. metadata +3 -3
  47. data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ class Addition
6
+ attr_reader :directives, :possible_types, :types, :union_memberships, :references, :arguments_with_default_values
7
+
8
+ def initialize(schema:, own_types:, new_types:)
9
+ @schema = schema
10
+ @own_types = own_types
11
+ @directives = Set.new
12
+ @possible_types = {}
13
+ @types = {}
14
+ @union_memberships = {}
15
+ @references = Hash.new { |h, k| h[k] = [] }
16
+ @arguments_with_default_values = []
17
+ add_type_and_traverse(new_types)
18
+ end
19
+
20
+ private
21
+
22
+ def references_to(thing, from:)
23
+ @references[thing] << from
24
+ end
25
+
26
+ def get_type(name)
27
+ @types[name] || @schema.get_type(name)
28
+ end
29
+
30
+ # Lookup using `own_types` here because it's ok to override
31
+ # inherited types by name
32
+ def get_local_type(name)
33
+ @types[name] || @own_types[name]
34
+ end
35
+
36
+ def add_directives_from(owner)
37
+ dirs = owner.directives.map(&:class)
38
+ @directives.merge(dirs)
39
+ add_type_and_traverse(dirs)
40
+ end
41
+
42
+ def add_type_and_traverse(new_types)
43
+ late_types = []
44
+ new_types.each { |t| add_type(t, owner: nil, late_types: late_types, path: [t.graphql_name]) }
45
+ missed_late_types = 0
46
+ while (late_type_vals = late_types.shift)
47
+ type_owner, lt = late_type_vals
48
+ if lt.is_a?(String)
49
+ type = Member::BuildType.constantize(lt)
50
+ # Reset the counter, since we might succeed next go-round
51
+ missed_late_types = 0
52
+ update_type_owner(type_owner, type)
53
+ add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name])
54
+ elsif lt.is_a?(LateBoundType)
55
+ if (type = get_type(lt.name))
56
+ # Reset the counter, since we might succeed next go-round
57
+ missed_late_types = 0
58
+ update_type_owner(type_owner, type)
59
+ add_type(type, owner: type_owner, late_types: late_types, path: [type.graphql_name])
60
+ else
61
+ missed_late_types += 1
62
+ # Add it back to the list, maybe we'll be able to resolve it later.
63
+ late_types << [type_owner, lt]
64
+ if missed_late_types == late_types.size
65
+ # We've looked at all of them and haven't resolved one.
66
+ raise UnresolvedLateBoundTypeError.new(type: lt)
67
+ else
68
+ # Try the next one
69
+ end
70
+ end
71
+ else
72
+ raise ArgumentError, "Unexpected late type: #{lt.inspect}"
73
+ end
74
+ end
75
+ nil
76
+ end
77
+
78
+ def update_type_owner(owner, type)
79
+ case owner
80
+ when Class
81
+ if owner.kind.union?
82
+ # It's a union with possible_types
83
+ # Replace the item by class name
84
+ owner.assign_type_membership_object_type(type)
85
+ @possible_types[owner.graphql_name] = owner.possible_types
86
+ elsif type.kind.interface? && owner.kind.object?
87
+ new_interfaces = []
88
+ owner.interfaces.each do |int_t|
89
+ if int_t.is_a?(String) && int_t == type.graphql_name
90
+ new_interfaces << type
91
+ elsif int_t.is_a?(LateBoundType) && int_t.graphql_name == type.graphql_name
92
+ new_interfaces << type
93
+ else
94
+ # Don't re-add proper interface definitions,
95
+ # they were probably already added, maybe with options.
96
+ end
97
+ end
98
+ owner.implements(*new_interfaces)
99
+ new_interfaces.each do |int|
100
+ pt = @possible_types[int.graphql_name] ||= []
101
+ if !pt.include?(owner)
102
+ pt << owner
103
+ end
104
+ end
105
+ end
106
+
107
+ when nil
108
+ # It's a root type
109
+ @types[type.graphql_name] = type
110
+ when GraphQL::Schema::Field, GraphQL::Schema::Argument
111
+ orig_type = owner.type
112
+ # Apply list/non-null wrapper as needed
113
+ if orig_type.respond_to?(:of_type)
114
+ transforms = []
115
+ while (orig_type.respond_to?(:of_type))
116
+ if orig_type.kind.non_null?
117
+ transforms << :to_non_null_type
118
+ elsif orig_type.kind.list?
119
+ transforms << :to_list_type
120
+ else
121
+ raise "Invariant: :of_type isn't non-null or list"
122
+ end
123
+ orig_type = orig_type.of_type
124
+ end
125
+ transforms.reverse_each { |t| type = type.public_send(t) }
126
+ end
127
+ owner.type = type
128
+ else
129
+ raise "Unexpected update: #{owner.inspect} #{type.inspect}"
130
+ end
131
+ end
132
+
133
+ def add_type(type, owner:, late_types:, path:)
134
+ if type.respond_to?(:metadata) && type.metadata.is_a?(Hash)
135
+ type_class = type.metadata[:type_class]
136
+ if type_class.nil?
137
+ raise ArgumentError, "Can't add legacy type: #{type} (#{type.class})"
138
+ else
139
+ type = type_class
140
+ end
141
+ elsif type.is_a?(String) || type.is_a?(GraphQL::Schema::LateBoundType)
142
+ late_types << [owner, type]
143
+ return
144
+ end
145
+
146
+ if owner.is_a?(Class) && owner < GraphQL::Schema::Union
147
+ um = @union_memberships[type.graphql_name] ||= []
148
+ um << owner
149
+ end
150
+
151
+ if (prev_type = get_local_type(type.graphql_name))
152
+ if prev_type != type
153
+ raise DuplicateTypeNamesError.new(
154
+ type_name: type.graphql_name,
155
+ first_definition: prev_type,
156
+ second_definition: type,
157
+ path: path,
158
+ )
159
+ else
160
+ # This type was already added
161
+ end
162
+ elsif type.is_a?(Class) && type < GraphQL::Schema::Directive
163
+ @directives << type
164
+ type.arguments.each do |name, arg|
165
+ arg_type = arg.type.unwrap
166
+ references_to(arg_type, from: arg)
167
+ add_type(arg_type, owner: arg, late_types: late_types, path: path + [name])
168
+ if arg.default_value?
169
+ @arguments_with_default_values << arg
170
+ end
171
+ end
172
+ else
173
+ @types[type.graphql_name] = type
174
+ add_directives_from(type)
175
+ if type.kind.fields?
176
+ type.fields.each do |name, field|
177
+ field_type = field.type.unwrap
178
+ references_to(field_type, from: field)
179
+ field_path = path + [name]
180
+ add_type(field_type, owner: field, late_types: late_types, path: field_path)
181
+ add_directives_from(field)
182
+ field.arguments.each do |arg_name, arg|
183
+ add_directives_from(arg)
184
+ arg_type = arg.type.unwrap
185
+ references_to(arg_type, from: arg)
186
+ add_type(arg_type, owner: arg, late_types: late_types, path: field_path + [arg_name])
187
+ if arg.default_value?
188
+ @arguments_with_default_values << arg
189
+ end
190
+ end
191
+ end
192
+ end
193
+ if type.kind.input_object?
194
+ type.arguments.each do |arg_name, arg|
195
+ add_directives_from(arg)
196
+ arg_type = arg.type.unwrap
197
+ references_to(arg_type, from: arg)
198
+ add_type(arg_type, owner: arg, late_types: late_types, path: path + [arg_name])
199
+ if arg.default_value?
200
+ @arguments_with_default_values << arg
201
+ end
202
+ end
203
+ end
204
+ if type.kind.union?
205
+ @possible_types[type.graphql_name] = type.possible_types
206
+ type.possible_types.each do |t|
207
+ add_type(t, owner: type, late_types: late_types, path: path + ["possible_types"])
208
+ end
209
+ end
210
+ if type.kind.interface?
211
+ type.orphan_types.each do |t|
212
+ add_type(t, owner: type, late_types: late_types, path: path + ["orphan_types"])
213
+ end
214
+ end
215
+ if type.kind.object?
216
+ @possible_types[type.graphql_name] = [type]
217
+ type.interface_type_memberships.each do |interface_type_membership|
218
+ case interface_type_membership
219
+ when Schema::TypeMembership
220
+ interface_type = interface_type_membership.abstract_type
221
+ # We can get these now; we'll have to get late-bound types later
222
+ if interface_type.is_a?(Module)
223
+ implementers = @possible_types[interface_type.graphql_name] ||= []
224
+ implementers << type
225
+ end
226
+ when String, Schema::LateBoundType
227
+ interface_type = interface_type_membership
228
+ else
229
+ raise ArgumentError, "Invariant: unexpected type membership for #{type.graphql_name}: #{interface_type_membership.class} (#{interface_type_membership.inspect})"
230
+ end
231
+ add_type(interface_type, owner: type, late_types: late_types, path: path + ["implements"])
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -240,60 +240,79 @@ module GraphQL
240
240
  def coerce_into_values(parent_object, values, context, argument_values)
241
241
  arg_name = graphql_name
242
242
  arg_key = keyword
243
- has_value = false
244
243
  default_used = false
244
+
245
245
  if values.key?(arg_name)
246
- has_value = true
247
246
  value = values[arg_name]
248
247
  elsif values.key?(arg_key)
249
- has_value = true
250
248
  value = values[arg_key]
251
249
  elsif default_value?
252
- has_value = true
253
250
  value = default_value
254
251
  default_used = true
252
+ else
253
+ # no value at all
254
+ owner.validate_directive_argument(self, nil)
255
+ return
255
256
  end
256
257
 
257
- if has_value
258
- loaded_value = nil
259
- coerced_value = context.schema.error_handler.with_error_handling(context) do
260
- type.coerce_input(value, context)
261
- end
258
+ loaded_value = nil
259
+ coerced_value = context.schema.error_handler.with_error_handling(context) do
260
+ type.coerce_input(value, context)
261
+ end
262
262
 
263
- # TODO this should probably be inside after_lazy
264
- if loads && !from_resolver?
265
- loaded_value = if type.list?
266
- loaded_values = coerced_value.map { |val| owner.load_application_object(self, loads, val, context) }
267
- context.schema.after_any_lazies(loaded_values) { |result| result }
268
- else
263
+ # TODO this should probably be inside after_lazy
264
+ if loads && !from_resolver?
265
+ loaded_value = if type.list?
266
+ loaded_values = coerced_value.map { |val| owner.load_application_object(self, loads, val, context) }
267
+ context.schema.after_any_lazies(loaded_values) { |result| result }
268
+ else
269
+ context.query.with_error_handling do
269
270
  owner.load_application_object(self, loads, coerced_value, context)
270
271
  end
271
272
  end
273
+ end
272
274
 
273
- coerced_value = if loaded_value
274
- loaded_value
275
- else
276
- coerced_value
275
+ coerced_value = if loaded_value
276
+ loaded_value
277
+ else
278
+ coerced_value
279
+ end
280
+
281
+ # If this isn't lazy, then the block returns eagerly and assigns the result here
282
+ # If it _is_ lazy, then we write the lazy to the hash, then update it later
283
+ argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |coerced_value|
284
+ owner.validate_directive_argument(self, coerced_value)
285
+ prepared_value = context.schema.error_handler.with_error_handling(context) do
286
+ prepare_value(parent_object, coerced_value, context: context)
277
287
  end
278
288
 
279
- # If this isn't lazy, then the block returns eagerly and assigns the result here
280
- # If it _is_ lazy, then we write the lazy to the hash, then update it later
281
- argument_values[arg_key] = context.schema.after_lazy(coerced_value) do |coerced_value|
282
- owner.validate_directive_argument(self, coerced_value)
283
- prepared_value = context.schema.error_handler.with_error_handling(context) do
284
- prepare_value(parent_object, coerced_value, context: context)
285
- end
289
+ # TODO code smell to access such a deeply-nested constant in a distant module
290
+ argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
291
+ value: prepared_value,
292
+ definition: self,
293
+ default_used: default_used,
294
+ )
295
+ end
296
+ end
286
297
 
287
- # TODO code smell to access such a deeply-nested constant in a distant module
288
- argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
289
- value: prepared_value,
290
- definition: self,
291
- default_used: default_used,
292
- )
293
- end
294
- else
295
- # has_value is false
296
- owner.validate_directive_argument(self, nil)
298
+ # @api private
299
+ def validate_default_value
300
+ coerced_default_value = begin
301
+ type.coerce_isolated_result(default_value) unless default_value.nil?
302
+ rescue GraphQL::Schema::Enum::UnresolvedValueError
303
+ # It raises this, which is helpful at runtime, but not here...
304
+ default_value
305
+ end
306
+ res = type.valid_isolated_input?(coerced_default_value)
307
+ if !res
308
+ raise InvalidDefaultValueError.new(self)
309
+ end
310
+ end
311
+
312
+ class InvalidDefaultValueError < GraphQL::Error
313
+ def initialize(argument)
314
+ message = "`#{argument.path}` has an invalid default value: `#{argument.default_value.inspect}` isn't accepted by `#{argument.type.to_type_signature}`; update the default value or the argument type."
315
+ super(message)
297
316
  end
298
317
  end
299
318
 
@@ -39,7 +39,19 @@ module GraphQL
39
39
  transform_name = arguments[:by]
40
40
  if TRANSFORMS.include?(transform_name) && return_value.respond_to?(transform_name)
41
41
  return_value = return_value.public_send(transform_name)
42
- context.namespace(:interpreter)[:runtime].write_in_response(path, return_value)
42
+ response = context.namespace(:interpreter)[:runtime].response
43
+ *keys, last = path
44
+ keys.each do |key|
45
+ if response && (response = response[key])
46
+ next
47
+ else
48
+ break
49
+ end
50
+ end
51
+ if response
52
+ response[last] = return_value
53
+ end
54
+ nil
43
55
  end
44
56
  end
45
57
  end
@@ -226,8 +226,8 @@ module GraphQL
226
226
  # It's funny to think of a _result_ of an input object.
227
227
  # This is used for rendering the default value in introspection responses.
228
228
  def coerce_result(value, ctx)
229
- # Allow the application to provide values as :symbols, and convert them to the strings
230
- value = value.reduce({}) { |memo, (k, v)| memo[k.to_s] = v; memo }
229
+ # Allow the application to provide values as :snake_symbols, and convert them to the camelStrings
230
+ value = value.reduce({}) { |memo, (k, v)| memo[Member::BuildType.camelize(k.to_s)] = v; memo }
231
231
 
232
232
  result = {}
233
233
 
@@ -169,6 +169,12 @@ module GraphQL
169
169
  def build_fields(type_defn, fields, type_resolver)
170
170
  loader = self
171
171
  fields.each do |field_hash|
172
+ unwrapped_field_hash = field_hash
173
+ while (of_type = unwrapped_field_hash["ofType"])
174
+ unwrapped_field_hash = of_type
175
+ end
176
+ type_name = unwrapped_field_hash["name"]
177
+
172
178
  type_defn.field(
173
179
  field_hash["name"],
174
180
  type: type_resolver.call(field_hash["type"]),
@@ -176,6 +182,8 @@ module GraphQL
176
182
  deprecation_reason: field_hash["deprecationReason"],
177
183
  null: true,
178
184
  camelize: false,
185
+ connection_extension: nil,
186
+ connection: type_name.end_with?("Connection"),
179
187
  ) do
180
188
  if field_hash["args"].any?
181
189
  loader.build_arguments(self, field_hash["args"], type_resolver)
@@ -113,27 +113,15 @@ module GraphQL
113
113
  end
114
114
 
115
115
  def visible?(context)
116
- if @mutation
117
- @mutation.visible?(context)
118
- else
119
- true
120
- end
116
+ true
121
117
  end
122
118
 
123
119
  def accessible?(context)
124
- if @mutation
125
- @mutation.accessible?(context)
126
- else
127
- true
128
- end
120
+ true
129
121
  end
130
122
 
131
123
  def authorized?(object, context)
132
- if @mutation
133
- @mutation.authorized?(object, context)
134
- else
135
- true
136
- end
124
+ true
137
125
  end
138
126
  end
139
127
  end
@@ -48,12 +48,26 @@ module GraphQL
48
48
  # @return [GraphQL::Schema::Object, GraphQL::Execution::Lazy]
49
49
  # @raise [GraphQL::UnauthorizedError] if the user-provided hook returns `false`
50
50
  def authorized_new(object, context)
51
- auth_val = context.query.with_error_handling do
52
- begin
53
- authorized?(object, context)
54
- rescue GraphQL::UnauthorizedError => err
55
- context.schema.unauthorized_object(err)
51
+ trace_payload = { context: context, type: self, object: object, path: context[:current_path] }
52
+
53
+ maybe_lazy_auth_val = context.query.trace("authorized", trace_payload) do
54
+ context.query.with_error_handling do
55
+ begin
56
+ authorized?(object, context)
57
+ rescue GraphQL::UnauthorizedError => err
58
+ context.schema.unauthorized_object(err)
59
+ end
60
+ end
61
+ end
62
+
63
+ auth_val = if context.schema.lazy?(maybe_lazy_auth_val)
64
+ GraphQL::Execution::Lazy.new do
65
+ context.query.trace("authorized_lazy", trace_payload) do
66
+ context.schema.sync_lazy(maybe_lazy_auth_val)
67
+ end
56
68
  end
69
+ else
70
+ maybe_lazy_auth_val
57
71
  end
58
72
 
59
73
  context.schema.after_lazy(auth_val) do |is_authorized|