graphql 1.12.7 → 1.12.12
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/install_generator.rb +1 -1
- data/lib/generators/graphql/templates/graphql_controller.erb +2 -2
- data/lib/graphql.rb +10 -10
- data/lib/graphql/backtrace/table.rb +14 -2
- data/lib/graphql/dataloader.rb +59 -15
- data/lib/graphql/dataloader/null_dataloader.rb +1 -0
- data/lib/graphql/execution/errors.rb +4 -4
- data/lib/graphql/execution/execute.rb +1 -1
- data/lib/graphql/execution/interpreter.rb +4 -8
- data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -2
- data/lib/graphql/execution/interpreter/runtime.rb +382 -223
- data/lib/graphql/introspection/schema_type.rb +1 -1
- data/lib/graphql/pagination/connections.rb +1 -1
- data/lib/graphql/query/null_context.rb +7 -1
- data/lib/graphql/rake_task.rb +3 -0
- data/lib/graphql/schema.rb +44 -218
- data/lib/graphql/schema/addition.rb +238 -0
- data/lib/graphql/schema/argument.rb +55 -36
- data/lib/graphql/schema/directive/transform.rb +13 -1
- data/lib/graphql/schema/input_object.rb +2 -2
- data/lib/graphql/schema/loader.rb +8 -0
- data/lib/graphql/schema/member/base_dsl_methods.rb +3 -15
- data/lib/graphql/schema/object.rb +19 -5
- data/lib/graphql/schema/resolver.rb +46 -24
- data/lib/graphql/schema/scalar.rb +3 -1
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -1
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +17 -8
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
- data/lib/graphql/static_validation/validator.rb +5 -0
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
- data/lib/graphql/subscriptions/serialize.rb +11 -1
- data/lib/graphql/version.rb +1 -1
- metadata +17 -3
- data/lib/graphql/execution/interpreter/hash_response.rb +0 -46
@@ -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
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
-
#
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
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].
|
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 :
|
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
|
-
|
117
|
-
@mutation.visible?(context)
|
118
|
-
else
|
119
|
-
true
|
120
|
-
end
|
116
|
+
true
|
121
117
|
end
|
122
118
|
|
123
119
|
def accessible?(context)
|
124
|
-
|
125
|
-
@mutation.accessible?(context)
|
126
|
-
else
|
127
|
-
true
|
128
|
-
end
|
120
|
+
true
|
129
121
|
end
|
130
122
|
|
131
123
|
def authorized?(object, context)
|
132
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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|
|
@@ -333,30 +333,32 @@ module GraphQL
|
|
333
333
|
arg_defn = super(*args, from_resolver: true, **kwargs)
|
334
334
|
own_arguments_loads_as_type[arg_defn.keyword] = loads if loads
|
335
335
|
|
336
|
-
if
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
336
|
+
if !method_defined?(:"load_#{arg_defn.keyword}")
|
337
|
+
if loads && arg_defn.type.list?
|
338
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
339
|
+
def load_#{arg_defn.keyword}(values)
|
340
|
+
argument = @arguments_by_keyword[:#{arg_defn.keyword}]
|
341
|
+
lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
|
342
|
+
context.schema.after_lazy(values) do |values2|
|
343
|
+
GraphQL::Execution::Lazy.all(values2.map { |value| load_application_object(argument, lookup_as_type, value, context) })
|
344
|
+
end
|
343
345
|
end
|
346
|
+
RUBY
|
347
|
+
elsif loads
|
348
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
349
|
+
def load_#{arg_defn.keyword}(value)
|
350
|
+
argument = @arguments_by_keyword[:#{arg_defn.keyword}]
|
351
|
+
lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
|
352
|
+
load_application_object(argument, lookup_as_type, value, context)
|
353
|
+
end
|
354
|
+
RUBY
|
355
|
+
else
|
356
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
357
|
+
def load_#{arg_defn.keyword}(value)
|
358
|
+
value
|
359
|
+
end
|
360
|
+
RUBY
|
344
361
|
end
|
345
|
-
RUBY
|
346
|
-
elsif loads
|
347
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
348
|
-
def load_#{arg_defn.keyword}(value)
|
349
|
-
argument = @arguments_by_keyword[:#{arg_defn.keyword}]
|
350
|
-
lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
|
351
|
-
load_application_object(argument, lookup_as_type, value, context)
|
352
|
-
end
|
353
|
-
RUBY
|
354
|
-
else
|
355
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
356
|
-
def load_#{arg_defn.keyword}(value)
|
357
|
-
value
|
358
|
-
end
|
359
|
-
RUBY
|
360
362
|
end
|
361
363
|
|
362
364
|
arg_defn
|
@@ -372,16 +374,36 @@ module GraphQL
|
|
372
374
|
# @param extension [Class] Extension class
|
373
375
|
# @param options [Hash] Optional extension options
|
374
376
|
def extension(extension, **options)
|
375
|
-
|
377
|
+
@own_extensions ||= []
|
378
|
+
@own_extensions << {extension => options}
|
376
379
|
end
|
377
380
|
|
378
381
|
# @api private
|
379
382
|
def extensions
|
380
|
-
|
383
|
+
own_exts = @own_extensions
|
384
|
+
# Jump through some hoops to avoid creating arrays when we don't actually need them
|
385
|
+
if superclass.respond_to?(:extensions)
|
386
|
+
s_exts = superclass.extensions
|
387
|
+
if own_exts
|
388
|
+
if s_exts.any?
|
389
|
+
own_exts + s_exts
|
390
|
+
else
|
391
|
+
own_exts
|
392
|
+
end
|
393
|
+
else
|
394
|
+
s_exts
|
395
|
+
end
|
396
|
+
else
|
397
|
+
own_exts || EMPTY_ARRAY
|
398
|
+
end
|
381
399
|
end
|
382
400
|
|
383
401
|
private
|
384
402
|
|
403
|
+
def own_extensions
|
404
|
+
@own_extensions
|
405
|
+
end
|
406
|
+
|
385
407
|
def own_arguments_loads_as_type
|
386
408
|
@own_arguments_loads_as_type ||= {}
|
387
409
|
end
|
@@ -44,7 +44,9 @@ module GraphQL
|
|
44
44
|
def validate_non_null_input(value, ctx)
|
45
45
|
result = Query::InputValidationResult.new
|
46
46
|
coerced_result = begin
|
47
|
-
|
47
|
+
ctx.query.with_error_handling do
|
48
|
+
coerce_input(value, ctx)
|
49
|
+
end
|
48
50
|
rescue GraphQL::CoercionError => err
|
49
51
|
err
|
50
52
|
end
|
@@ -373,17 +373,26 @@ module GraphQL
|
|
373
373
|
# In this context, `parents` represends the "self scope" of the field,
|
374
374
|
# what types may be found at this point in the query.
|
375
375
|
def mutually_exclusive?(parents1, parents2)
|
376
|
-
parents1.
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
376
|
+
if parents1.empty? || parents2.empty?
|
377
|
+
false
|
378
|
+
elsif parents1.length == parents2.length
|
379
|
+
parents1.length.times.any? do |i|
|
380
|
+
type1 = parents1[i - 1]
|
381
|
+
type2 = parents2[i - 1]
|
382
|
+
if type1 == type2
|
383
|
+
# If the types we're comparing are the same type,
|
384
|
+
# then they aren't mutually exclusive
|
385
|
+
false
|
386
|
+
else
|
387
|
+
# Check if these two scopes have _any_ types in common.
|
388
|
+
possible_right_types = context.query.possible_types(type1)
|
389
|
+
possible_left_types = context.query.possible_types(type2)
|
390
|
+
(possible_right_types & possible_left_types).empty?
|
382
391
|
end
|
383
392
|
end
|
393
|
+
else
|
394
|
+
true
|
384
395
|
end
|
385
|
-
|
386
|
-
false
|
387
396
|
end
|
388
397
|
end
|
389
398
|
end
|
@@ -23,7 +23,7 @@ module GraphQL
|
|
23
23
|
defn = if arg_defn && arg_defn.type.unwrap.kind.input_object?
|
24
24
|
arg_defn.type.unwrap
|
25
25
|
else
|
26
|
-
context.field_definition
|
26
|
+
context.directive_definition || context.field_definition
|
27
27
|
end
|
28
28
|
|
29
29
|
parent_type = context.warden.get_argument(defn, parent_name(parent, defn))
|
@@ -34,12 +34,12 @@ module GraphQL
|
|
34
34
|
# channel: self,
|
35
35
|
# }
|
36
36
|
#
|
37
|
-
# result = MySchema.execute(
|
37
|
+
# result = MySchema.execute(
|
38
38
|
# query: query,
|
39
39
|
# context: context,
|
40
40
|
# variables: variables,
|
41
41
|
# operation_name: operation_name
|
42
|
-
#
|
42
|
+
# )
|
43
43
|
#
|
44
44
|
# payload = {
|
45
45
|
# result: result.to_h,
|
@@ -146,14 +146,15 @@ module GraphQL
|
|
146
146
|
def setup_stream(channel, initial_event)
|
147
147
|
topic = initial_event.topic
|
148
148
|
channel.stream_from(stream_event_name(initial_event), coder: @action_cable_coder) do |message|
|
149
|
-
object = @serializer.load(message)
|
150
149
|
events_by_fingerprint = @events[topic]
|
150
|
+
object = nil
|
151
151
|
events_by_fingerprint.each do |_fingerprint, events|
|
152
152
|
if events.any? && events.first == initial_event
|
153
153
|
# The fingerprint has told us that this response should be shared by all subscribers,
|
154
154
|
# so just run it once, then deliver the result to every subscriber
|
155
155
|
first_event = events.first
|
156
156
|
first_subscription_id = first_event.context.fetch(:subscription_id)
|
157
|
+
object ||= @serializer.load(message)
|
157
158
|
result = execute_update(first_subscription_id, first_event, object)
|
158
159
|
# Having calculated the result _once_, send the same payload to all subscribers
|
159
160
|
events.each do |event|
|
@@ -55,7 +55,14 @@ module GraphQL
|
|
55
55
|
# @return [Object] An object that load Global::Identification recursive
|
56
56
|
def load_value(value)
|
57
57
|
if value.is_a?(Array)
|
58
|
-
value.
|
58
|
+
is_gids = (v1 = value[0]).is_a?(Hash) && v1.size == 1 && v1[GLOBALID_KEY]
|
59
|
+
if is_gids
|
60
|
+
# Assume it's an array of global IDs
|
61
|
+
ids = value.map { |v| v[GLOBALID_KEY] }
|
62
|
+
GlobalID::Locator.locate_many(ids)
|
63
|
+
else
|
64
|
+
value.map { |item| load_value(item) }
|
65
|
+
end
|
59
66
|
elsif value.is_a?(Hash)
|
60
67
|
if value.size == 1
|
61
68
|
case value.keys.first # there's only 1 key
|
@@ -70,6 +77,9 @@ module GraphQL
|
|
70
77
|
when OPEN_STRUCT_KEY
|
71
78
|
ostruct_values = load_value(value[OPEN_STRUCT_KEY])
|
72
79
|
OpenStruct.new(ostruct_values)
|
80
|
+
else
|
81
|
+
key = value.keys.first
|
82
|
+
{ key => load_value(value[key]) }
|
73
83
|
end
|
74
84
|
else
|
75
85
|
loaded_h = {}
|