graphql 2.3.5 → 2.3.11

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.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +46 -0
  3. data/lib/graphql/analysis/analyzer.rb +89 -0
  4. data/lib/graphql/analysis/field_usage.rb +82 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +20 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +20 -0
  7. data/lib/graphql/analysis/query_complexity.rb +183 -0
  8. data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
  9. data/lib/graphql/analysis/visitor.rb +283 -0
  10. data/lib/graphql/analysis.rb +92 -1
  11. data/lib/graphql/current.rb +52 -0
  12. data/lib/graphql/dataloader/async_dataloader.rb +2 -0
  13. data/lib/graphql/dataloader/source.rb +5 -2
  14. data/lib/graphql/dataloader.rb +4 -1
  15. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  16. data/lib/graphql/execution/interpreter/runtime.rb +8 -14
  17. data/lib/graphql/execution/interpreter.rb +3 -1
  18. data/lib/graphql/execution/lookahead.rb +10 -10
  19. data/lib/graphql/introspection/directive_type.rb +1 -1
  20. data/lib/graphql/introspection/entry_points.rb +2 -2
  21. data/lib/graphql/introspection/field_type.rb +1 -1
  22. data/lib/graphql/introspection/schema_type.rb +6 -11
  23. data/lib/graphql/introspection/type_type.rb +5 -5
  24. data/lib/graphql/language/document_from_schema_definition.rb +19 -26
  25. data/lib/graphql/language/lexer.rb +0 -3
  26. data/lib/graphql/language/nodes.rb +2 -2
  27. data/lib/graphql/language/parser.rb +9 -1
  28. data/lib/graphql/language/sanitized_printer.rb +1 -1
  29. data/lib/graphql/language.rb +0 -1
  30. data/lib/graphql/query/context.rb +7 -1
  31. data/lib/graphql/query/null_context.rb +2 -2
  32. data/lib/graphql/query/validation_pipeline.rb +2 -2
  33. data/lib/graphql/query.rb +26 -7
  34. data/lib/graphql/schema/always_visible.rb +1 -0
  35. data/lib/graphql/schema/argument.rb +19 -5
  36. data/lib/graphql/schema/build_from_definition.rb +8 -1
  37. data/lib/graphql/schema/directive/flagged.rb +1 -1
  38. data/lib/graphql/schema/directive.rb +2 -0
  39. data/lib/graphql/schema/enum.rb +9 -5
  40. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  41. data/lib/graphql/schema/field.rb +13 -1
  42. data/lib/graphql/schema/has_single_input_argument.rb +2 -1
  43. data/lib/graphql/schema/input_object.rb +8 -7
  44. data/lib/graphql/schema/interface.rb +20 -4
  45. data/lib/graphql/schema/introspection_system.rb +5 -16
  46. data/lib/graphql/schema/member/has_arguments.rb +14 -9
  47. data/lib/graphql/schema/member/has_fields.rb +6 -4
  48. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  49. data/lib/graphql/schema/resolver.rb +5 -5
  50. data/lib/graphql/schema/subset.rb +510 -0
  51. data/lib/graphql/schema/type_expression.rb +2 -2
  52. data/lib/graphql/schema/types_migration.rb +185 -0
  53. data/lib/graphql/schema/validator/all_validator.rb +60 -0
  54. data/lib/graphql/schema/validator.rb +2 -0
  55. data/lib/graphql/schema/warden.rb +89 -5
  56. data/lib/graphql/schema.rb +74 -37
  57. data/lib/graphql/static_validation/base_visitor.rb +6 -5
  58. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  59. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  60. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
  61. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -2
  62. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +1 -1
  63. data/lib/graphql/static_validation/rules/fields_will_merge.rb +7 -7
  64. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  65. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +1 -1
  66. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  67. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  68. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  69. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -3
  70. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
  71. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +1 -1
  72. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  73. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  74. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
  75. data/lib/graphql/static_validation/validation_context.rb +2 -2
  76. data/lib/graphql/subscriptions/broadcast_analyzer.rb +11 -5
  77. data/lib/graphql/subscriptions/event.rb +1 -1
  78. data/lib/graphql/subscriptions.rb +3 -3
  79. data/lib/graphql/testing/helpers.rb +8 -5
  80. data/lib/graphql/types/relay/connection_behaviors.rb +10 -0
  81. data/lib/graphql/types/relay/edge_behaviors.rb +10 -0
  82. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  83. data/lib/graphql/version.rb +1 -1
  84. data/lib/graphql.rb +1 -0
  85. metadata +14 -13
  86. data/lib/graphql/analysis/ast/analyzer.rb +0 -91
  87. data/lib/graphql/analysis/ast/field_usage.rb +0 -84
  88. data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
  89. data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
  90. data/lib/graphql/analysis/ast/query_complexity.rb +0 -185
  91. data/lib/graphql/analysis/ast/visitor.rb +0 -284
  92. data/lib/graphql/analysis/ast.rb +0 -94
  93. data/lib/graphql/language/token.rb +0 -34
  94. data/lib/graphql/schema/invalid_type_error.rb +0 -7
@@ -0,0 +1,510 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ # This class filters the types, fields, arguments, enum values, and directives in a schema
6
+ # based on the given `context`.
7
+ #
8
+ # It's like {Warden}, but has some differences:
9
+ #
10
+ # - It doesn't use {Schema}'s top-level caches (eg {Schema.references_to}, {Schema.possible_types}, {Schema.types})
11
+ # - It doesn't hide Interface or Union types when all their possible types are hidden. (Instead, those types should implement `.visible?` to hide in that case.)
12
+ # - It checks `.visible?` on root introspection types
13
+ #
14
+ # In the future, {Subset} will support lazy-loading types as needed during execution and multi-request caching of subsets.
15
+ #
16
+ # @see Schema::TypesMigration for a helper class in adopting this filter
17
+ class Subset
18
+ # @return [Schema::Subset]
19
+ def self.from_context(ctx, schema)
20
+ if ctx.respond_to?(:types) && (types = ctx.types).is_a?(self)
21
+ types
22
+ else
23
+ # TODO use a cached instance from the schema
24
+ self.new(context: ctx, schema: schema)
25
+ end
26
+ end
27
+
28
+ def self.pass_thru(context:, schema:)
29
+ subset = self.new(context: context, schema: schema)
30
+ subset.instance_variable_set(:@cached_visible, Hash.new { |h,k| h[k] = true })
31
+ subset
32
+ end
33
+
34
+ def initialize(context:, schema:)
35
+ @context = context
36
+ @schema = schema
37
+ @all_types = {}
38
+ @all_types_loaded = false
39
+ @unvisited_types = []
40
+ @referenced_types = Hash.new { |h, type_defn| h[type_defn] = [] }.compare_by_identity
41
+ @cached_directives = {}
42
+ @all_directives = nil
43
+ @cached_visible = Hash.new { |h, member|
44
+ h[member] = @schema.visible?(member, @context)
45
+ }.compare_by_identity
46
+
47
+ @cached_visible_fields = Hash.new { |h, owner|
48
+ h[owner] = Hash.new do |h2, field|
49
+ h2[field] = if @cached_visible[field] &&
50
+ (ret_type = field.type.unwrap) &&
51
+ @cached_visible[ret_type] &&
52
+ (owner == field.owner || (!owner.kind.object?) || field_on_visible_interface?(field, owner))
53
+
54
+ if !field.introspection?
55
+ # The problem is that some introspection fields may have references
56
+ # to non-custom introspection types.
57
+ # If those were added here, they'd cause a DuplicateNamesError.
58
+ # This is basically a bug -- those fields _should_ reference the custom types.
59
+ add_type(ret_type, field)
60
+ end
61
+ true
62
+ else
63
+ false
64
+ end
65
+ end.compare_by_identity
66
+ }.compare_by_identity
67
+
68
+ @cached_visible_arguments = Hash.new do |h, arg|
69
+ h[arg] = if @cached_visible[arg] && (arg_type = arg.type.unwrap) && @cached_visible[arg_type]
70
+ add_type(arg_type, arg)
71
+ true
72
+ else
73
+ false
74
+ end
75
+ end.compare_by_identity
76
+
77
+ @cached_parent_fields = Hash.new do |h, type|
78
+ h[type] = Hash.new do |h2, field_name|
79
+ h2[field_name] = type.get_field(field_name, @context)
80
+ end
81
+ end.compare_by_identity
82
+
83
+ @cached_parent_arguments = Hash.new do |h, arg_owner|
84
+ h[arg_owner] = Hash.new do |h2, arg_name|
85
+ h2[arg_name] = arg_owner.get_argument(arg_name, @context)
86
+ end
87
+ end.compare_by_identity
88
+
89
+ @cached_possible_types = Hash.new do |h, type|
90
+ h[type] = case type.kind.name
91
+ when "INTERFACE"
92
+ load_all_types
93
+ pts = []
94
+ @unfiltered_interface_type_memberships[type].each { |itm|
95
+ if @cached_visible[itm] && (ot = itm.object_type) && @cached_visible[ot] && referenced?(ot)
96
+ pts << ot
97
+ end
98
+ }
99
+ pts
100
+ when "UNION"
101
+ pts = []
102
+ type.type_memberships.each { |tm|
103
+ if @cached_visible[tm] &&
104
+ (ot = tm.object_type) &&
105
+ @cached_visible[ot] &&
106
+ referenced?(ot)
107
+ pts << ot
108
+ end
109
+ }
110
+ pts
111
+ when "OBJECT"
112
+ load_all_types
113
+ if @all_types[type.graphql_name] == type
114
+ [type]
115
+ else
116
+ EmptyObjects::EMPTY_ARRAY
117
+ end
118
+ else
119
+ GraphQL::EmptyObjects::EMPTY_ARRAY
120
+ end
121
+ end.compare_by_identity
122
+
123
+ @cached_enum_values = Hash.new do |h, enum_t|
124
+ values = non_duplicate_items(enum_t.all_enum_value_definitions, @cached_visible)
125
+ if values.size == 0
126
+ raise GraphQL::Schema::Enum::MissingValuesError.new(enum_t)
127
+ end
128
+ h[enum_t] = values
129
+ end.compare_by_identity
130
+
131
+ @cached_fields = Hash.new do |h, owner|
132
+ h[owner] = non_duplicate_items(owner.all_field_definitions, @cached_visible_fields[owner])
133
+ end.compare_by_identity
134
+
135
+ @cached_arguments = Hash.new do |h, owner|
136
+ h[owner] = non_duplicate_items(owner.all_argument_definitions, @cached_visible_arguments)
137
+ end.compare_by_identity
138
+ end
139
+
140
+ def field_on_visible_interface?(field, owner)
141
+ ints = owner.interface_type_memberships.map(&:abstract_type)
142
+ field_name = field.graphql_name
143
+ filtered_ints = interfaces(owner)
144
+ any_interface_has_field = false
145
+ any_interface_has_visible_field = false
146
+ ints.each do |int_t|
147
+ if (_int_f_defn = @cached_parent_fields[int_t][field_name])
148
+ any_interface_has_field = true
149
+
150
+ if filtered_ints.include?(int_t) # TODO cycles, or maybe not necessary since previously checked? && @cached_visible_fields[owner][field]
151
+ any_interface_has_visible_field = true
152
+ break
153
+ end
154
+ end
155
+ end
156
+
157
+ if any_interface_has_field
158
+ any_interface_has_visible_field
159
+ else
160
+ true
161
+ end
162
+ end
163
+
164
+ def type(type_name)
165
+ t = if (loaded_t = @all_types[type_name])
166
+ loaded_t
167
+ elsif !@all_types_loaded
168
+ load_all_types
169
+ @all_types[type_name]
170
+ end
171
+ if t
172
+ if t.is_a?(Array)
173
+ vis_t = nil
174
+ t.each do |t_defn|
175
+ if @cached_visible[t_defn]
176
+ if vis_t.nil?
177
+ vis_t = t_defn
178
+ else
179
+ raise_duplicate_definition(vis_t, t_defn)
180
+ end
181
+ end
182
+ end
183
+ vis_t
184
+ else
185
+ if t && @cached_visible[t]
186
+ t
187
+ else
188
+ nil
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def field(owner, field_name)
195
+ f = if owner.kind.fields? && (field = @cached_parent_fields[owner][field_name])
196
+ field
197
+ elsif owner == query_root && (entry_point_field = @schema.introspection_system.entry_point(name: field_name))
198
+ entry_point_field
199
+ elsif (dynamic_field = @schema.introspection_system.dynamic_field(name: field_name))
200
+ dynamic_field
201
+ else
202
+ nil
203
+ end
204
+ if f.is_a?(Array)
205
+ visible_f = nil
206
+ f.each do |f_defn|
207
+ if @cached_visible_fields[owner][f_defn]
208
+
209
+ if visible_f.nil?
210
+ visible_f = f_defn
211
+ else
212
+ raise_duplicate_definition(visible_f, f_defn)
213
+ end
214
+ end
215
+ end
216
+ visible_f
217
+ else
218
+ if f && @cached_visible_fields[owner][f]
219
+ f
220
+ else
221
+ nil
222
+ end
223
+ end
224
+ end
225
+
226
+ def fields(owner)
227
+ @cached_fields[owner]
228
+ end
229
+
230
+ def arguments(owner)
231
+ @cached_arguments[owner]
232
+ end
233
+
234
+ def argument(owner, arg_name)
235
+ arg = @cached_parent_arguments[owner][arg_name]
236
+ if arg.is_a?(Array)
237
+ visible_arg = nil
238
+ arg.each do |arg_defn|
239
+ if @cached_visible_arguments[arg_defn]
240
+ if visible_arg.nil?
241
+ visible_arg = arg_defn
242
+ else
243
+ raise_duplicate_definition(visible_arg, arg_defn)
244
+ end
245
+ end
246
+ end
247
+ visible_arg
248
+ else
249
+ if arg && @cached_visible_arguments[arg]
250
+ arg
251
+ else
252
+ nil
253
+ end
254
+ end
255
+ end
256
+
257
+ def possible_types(type)
258
+ @cached_possible_types[type]
259
+ end
260
+
261
+ def interfaces(obj_or_int_type)
262
+ ints = obj_or_int_type.interface_type_memberships
263
+ .select { |itm| @cached_visible[itm] && @cached_visible[itm.abstract_type] }
264
+ .map!(&:abstract_type)
265
+ ints.uniq! # Remove any duplicate interfaces implemented via other interfaces
266
+ ints
267
+ end
268
+
269
+ def query_root
270
+ add_if_visible(@schema.query)
271
+ end
272
+
273
+ def mutation_root
274
+ add_if_visible(@schema.mutation)
275
+ end
276
+
277
+ def subscription_root
278
+ add_if_visible(@schema.subscription)
279
+ end
280
+
281
+ def all_types
282
+ load_all_types
283
+ @all_types.values
284
+ end
285
+
286
+ def all_types_h
287
+ load_all_types
288
+ @all_types
289
+ end
290
+
291
+ def enum_values(owner)
292
+ @cached_enum_values[owner]
293
+ end
294
+
295
+ def directive_exists?(dir_name)
296
+ if (dir = @schema.directives[dir_name]) && @cached_visible[dir]
297
+ !!dir
298
+ else
299
+ load_all_types
300
+ !!@cached_directives[dir_name]
301
+ end
302
+ end
303
+
304
+ def directives
305
+ @all_directives ||= begin
306
+ load_all_types
307
+ dirs = []
308
+ @schema.directives.each do |name, dir_defn|
309
+ if !@cached_directives[name] && @cached_visible[dir_defn]
310
+ dirs << dir_defn
311
+ end
312
+ end
313
+ dirs.concat(@cached_directives.values)
314
+ end
315
+ end
316
+
317
+ def loadable?(t, _ctx)
318
+ !@all_types[t.graphql_name] && @cached_visible[t]
319
+ end
320
+
321
+ def loaded_types
322
+ @all_types.values
323
+ end
324
+
325
+ def reachable_type?(name)
326
+ load_all_types
327
+ !!@all_types[name]
328
+ end
329
+
330
+ private
331
+
332
+ def add_if_visible(t)
333
+ (t && @cached_visible[t]) ? (add_type(t, true); t) : nil
334
+ end
335
+
336
+ def add_type(t, by_member)
337
+ if t && @cached_visible[t]
338
+ n = t.graphql_name
339
+ if (prev_t = @all_types[n])
340
+ if !prev_t.equal?(t)
341
+ raise_duplicate_definition(prev_t, t)
342
+ end
343
+ false
344
+ else
345
+ @referenced_types[t] << by_member
346
+ @all_types[n] = t
347
+ @unvisited_types << t
348
+ true
349
+ end
350
+ else
351
+ false
352
+ end
353
+ end
354
+
355
+ def non_duplicate_items(definitions, visibility_cache)
356
+ non_dups = []
357
+ definitions.each do |defn|
358
+ if visibility_cache[defn]
359
+ if (dup_defn = non_dups.find { |d| d.graphql_name == defn.graphql_name })
360
+ raise_duplicate_definition(dup_defn, defn)
361
+ end
362
+ non_dups << defn
363
+ end
364
+ end
365
+ non_dups
366
+ end
367
+
368
+ def raise_duplicate_definition(first_defn, second_defn)
369
+ raise DuplicateNamesError.new(duplicated_name: first_defn.path, duplicated_definition_1: first_defn.inspect, duplicated_definition_2: second_defn.inspect)
370
+ end
371
+
372
+ def referenced?(t)
373
+ load_all_types
374
+ @referenced_types[t].any? { |reference| (reference == true) || @cached_visible[reference] }
375
+ end
376
+
377
+ def load_all_types
378
+ return if @all_types_loaded
379
+ @all_types_loaded = true
380
+ entry_point_types = [
381
+ query_root,
382
+ mutation_root,
383
+ subscription_root,
384
+ *@schema.introspection_system.types.values,
385
+ ]
386
+
387
+ # Don't include any orphan_types whose interfaces aren't visible.
388
+ @schema.orphan_types.each do |orphan_type|
389
+ if @cached_visible[orphan_type] &&
390
+ orphan_type.interface_type_memberships.any? { |tm| @cached_visible[tm] && @cached_visible[tm.abstract_type] }
391
+ entry_point_types << orphan_type
392
+ end
393
+ end
394
+
395
+ @schema.directives.each do |_dir_name, dir_class|
396
+ if @cached_visible[dir_class]
397
+ arguments(dir_class).each do |arg|
398
+ entry_point_types << arg.type.unwrap
399
+ end
400
+ end
401
+ end
402
+
403
+ entry_point_types.compact! # TODO why is this necessary?!
404
+ entry_point_types.flatten! # handle multiple defns
405
+ entry_point_types.each { |t| add_type(t, true) }
406
+
407
+ @unfiltered_interface_type_memberships = Hash.new { |h, k| h[k] = [] }.compare_by_identity
408
+ @add_possible_types = Set.new
409
+
410
+ while @unvisited_types.any?
411
+ while t = @unvisited_types.pop
412
+ # These have already been checked for `.visible?`
413
+ visit_type(t)
414
+ end
415
+ @add_possible_types.each do |int_t|
416
+ itms = @unfiltered_interface_type_memberships[int_t]
417
+ itms.each do |itm|
418
+ if @cached_visible[itm] && (obj_type = itm.object_type) && @cached_visible[obj_type]
419
+ add_type(obj_type, itm)
420
+ end
421
+ end
422
+ end
423
+ @add_possible_types.clear
424
+ end
425
+
426
+ @all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) }
427
+ nil
428
+ end
429
+
430
+ def visit_type(type)
431
+ visit_directives(type)
432
+ case type.kind.name
433
+ when "OBJECT", "INTERFACE"
434
+ if type.kind.object?
435
+ type.interface_type_memberships.each do |itm|
436
+ @unfiltered_interface_type_memberships[itm.abstract_type] << itm
437
+ end
438
+ # recurse into visible implemented interfaces
439
+ interfaces(type).each do |interface|
440
+ add_type(interface, type)
441
+ end
442
+ else
443
+ type.orphan_types.each { |t| add_type(t, type)}
444
+ end
445
+
446
+ # recurse into visible fields
447
+ t_f = type.all_field_definitions
448
+ t_f.each do |field|
449
+ if @cached_visible[field]
450
+ visit_directives(field)
451
+ field_type = field.type.unwrap
452
+ if field_type.kind.interface?
453
+ @add_possible_types.add(field_type)
454
+ end
455
+ add_type(field_type, field)
456
+
457
+ # recurse into visible arguments
458
+ arguments(field).each do |argument|
459
+ visit_directives(argument)
460
+ add_type(argument.type.unwrap, argument)
461
+ end
462
+ end
463
+ end
464
+ when "INPUT_OBJECT"
465
+ # recurse into visible arguments
466
+ arguments(type).each do |argument|
467
+ visit_directives(argument)
468
+ add_type(argument.type.unwrap, argument)
469
+ end
470
+ when "UNION"
471
+ # recurse into visible possible types
472
+ type.type_memberships.each do |tm|
473
+ if @cached_visible[tm]
474
+ obj_t = tm.object_type
475
+ if obj_t.is_a?(String)
476
+ obj_t = Member::BuildType.constantize(obj_t)
477
+ tm.object_type = obj_t
478
+ end
479
+ if @cached_visible[obj_t]
480
+ add_type(obj_t, tm)
481
+ end
482
+ end
483
+ end
484
+ when "ENUM"
485
+ enum_values(type).each do |val|
486
+ visit_directives(val)
487
+ end
488
+ when "SCALAR"
489
+ # pass
490
+ end
491
+ end
492
+
493
+ def visit_directives(member)
494
+ member.directives.each { |dir|
495
+ dir_class = dir.class
496
+ if @cached_visible[dir_class]
497
+ dir_name = dir_class.graphql_name
498
+ if (existing_dir = @cached_directives[dir_name])
499
+ if existing_dir != dir_class
500
+ raise ArgumentError, "Two directives for `@#{dir_name}`: #{existing_dir}, #{dir.class}"
501
+ end
502
+ else
503
+ @cached_directives[dir.graphql_name] = dir_class
504
+ end
505
+ end
506
+ }
507
+ end
508
+ end
509
+ end
510
+ end
@@ -5,13 +5,13 @@ module GraphQL
5
5
  module TypeExpression
6
6
  # Fetch a type from a type map by its AST specification.
7
7
  # Return `nil` if not found.
8
- # @param type_owner [#get_type] A thing for looking up types by name
8
+ # @param type_owner [#type] A thing for looking up types by name
9
9
  # @param ast_node [GraphQL::Language::Nodes::AbstractNode]
10
10
  # @return [Class, GraphQL::Schema::NonNull, GraphQL::Schema:List]
11
11
  def self.build_type(type_owner, ast_node)
12
12
  case ast_node
13
13
  when GraphQL::Language::Nodes::TypeName
14
- type_owner.get_type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware
14
+ type_owner.type(ast_node.name) # rubocop:disable Development/ContextIsPassedCop -- this is a `context` or `warden`, it's already query-aware
15
15
  when GraphQL::Language::Nodes::NonNullType
16
16
  ast_inner_type = ast_node.of_type
17
17
  inner_type = build_type(type_owner, ast_inner_type)
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ # You can add this plugin to your schema to see how {GraphQL::Schema::Warden} and {GraphQL::Schema::Subset}
5
+ # handle `.visible?` differently in your schema.
6
+ #
7
+ # This plugin runs the same method on both implementations and raises an error when the results diverge.
8
+ #
9
+ # To fix the error, modify your schema so that both implementations return the same thing.
10
+ # Or, open an issue on GitHub to discuss the difference.
11
+ #
12
+ # This plugin adds overhead to runtime and may cause unexpected crashes -- **don't** use it in production!
13
+ #
14
+ # This plugin adds two keys to `context` when running:
15
+ #
16
+ # - `types_migration_running: true`
17
+ # - For the {Warden} which it instantiates, it adds `types_migration_warden_running: true`.
18
+ #
19
+ # Use those keys to modify your `visible?` behavior as needed.
20
+ #
21
+ # Also, in a pinch, you can set `skip_types_migration_error: true` in context to turn off this plugin's behavior per-query.
22
+ # (In that case, it uses {Subset} directly.)
23
+ #
24
+ # @example Adding this plugin
25
+ #
26
+ # if !Rails.env.production?
27
+ # use GraphQL::Schema::TypesMigration
28
+ # end
29
+ class TypesMigration < GraphQL::Schema::Subset
30
+ def self.use(schema)
31
+ schema.subset_class = self
32
+ end
33
+
34
+ class RuntimeTypesMismatchError < GraphQL::Error
35
+ def initialize(method_called, warden_result, subset_result, method_args)
36
+ super(<<~ERR)
37
+ Mismatch in types for `##{method_called}(#{method_args.map(&:inspect).join(", ")})`:
38
+
39
+ #{compare_results(warden_result, subset_result)}
40
+
41
+ Update your `.visible?` implementation to make these implementations return the same value.
42
+
43
+ See: https://graphql-ruby.org/authorization/visibility_migration.html
44
+ ERR
45
+ end
46
+
47
+ private
48
+ def compare_results(warden_result, subset_result)
49
+ if warden_result.is_a?(Array) && subset_result.is_a?(Array)
50
+ all_results = warden_result | subset_result
51
+ all_results.sort_by!(&:graphql_name)
52
+
53
+ entries_text = all_results.map { |entry| "#{entry.graphql_name} (#{entry})"}
54
+ width = entries_text.map(&:size).max
55
+ yes = " ✔ "
56
+ no = " "
57
+ res = "".dup
58
+ res << "#{"Result".center(width)} Warden Subset \n"
59
+ all_results.each_with_index do |entry, idx|
60
+ res << "#{entries_text[idx].ljust(width)}#{warden_result.include?(entry) ? yes : no}#{subset_result.include?(entry) ? yes : no}\n"
61
+ end
62
+ res << "\n"
63
+ else
64
+ "- Warden returned: #{humanize(warden_result)}\n\n- Subset returned: #{humanize(subset_result)}"
65
+ end
66
+ end
67
+ def humanize(val)
68
+ case val
69
+ when Array
70
+ "#{val.size}: #{val.map { |v| humanize(v) }.sort.inspect}"
71
+ when Module
72
+ if val.respond_to?(:graphql_name)
73
+ "#{val.graphql_name} (#{val.inspect})"
74
+ else
75
+ val.inspect
76
+ end
77
+ else
78
+ val.inspect
79
+ end
80
+ end
81
+ end
82
+
83
+ def initialize(context:, schema:)
84
+ @skip_error = context[:skip_types_migration_error]
85
+ context[:types_migration_running] = true
86
+ @subset_types = GraphQL::Schema::Subset.new(context: context, schema: schema)
87
+ if !@skip_error
88
+ warden_ctx_vals = context.to_h.dup
89
+ warden_ctx_vals[:types_migration_warden_running] = true
90
+ if defined?(schema::WardenCompatSchema)
91
+ warden_schema = schema::WardenCompatSchema
92
+ else
93
+ warden_schema = Class.new(schema)
94
+ warden_schema.use_schema_subset = false
95
+ # TODO public API
96
+ warden_schema.send(:add_type_and_traverse, [warden_schema.query, warden_schema.mutation, warden_schema.subscription].compact, root: true)
97
+ warden_schema.send(:add_type_and_traverse, warden_schema.directives.values + warden_schema.orphan_types, root: false)
98
+ end
99
+ warden_ctx = GraphQL::Query::Context.new(query: context.query, values: warden_ctx_vals)
100
+ example_warden = GraphQL::Schema::Warden.new(schema: warden_schema, context: warden_ctx)
101
+ @warden_types = example_warden.schema_subset
102
+ warden_ctx.warden = example_warden
103
+ warden_ctx.types = @warden_types
104
+ end
105
+ end
106
+
107
+ def loaded_types
108
+ @subset_types.loaded_types
109
+ end
110
+
111
+ PUBLIC_SUBSET_METHODS = [
112
+ :enum_values,
113
+ :interfaces,
114
+ :all_types,
115
+ :fields,
116
+ :loadable?,
117
+ :type,
118
+ :arguments,
119
+ :argument,
120
+ :directive_exists?,
121
+ :directives,
122
+ :field,
123
+ :query_root,
124
+ :mutation_root,
125
+ :possible_types,
126
+ :subscription_root,
127
+ :reachable_type?
128
+ ]
129
+
130
+ PUBLIC_SUBSET_METHODS.each do |subset_method|
131
+ define_method(subset_method) do |*args|
132
+ call_method_and_compare(subset_method, args)
133
+ end
134
+ end
135
+
136
+ def call_method_and_compare(method, args)
137
+ res_1 = @subset_types.public_send(method, *args)
138
+ if @skip_error
139
+ return res_1
140
+ end
141
+
142
+ res_2 = @warden_types.public_send(method, *args)
143
+ normalized_res_1 = res_1.is_a?(Array) ? Set.new(res_1) : res_1
144
+ normalized_res_2 = res_2.is_a?(Array) ? Set.new(res_2) : res_2
145
+ if !equivalent_schema_members?(normalized_res_1, normalized_res_2)
146
+ # Raise the errors with the orignally returned values:
147
+ err = RuntimeTypesMismatchError.new(method, res_2, res_1, args)
148
+ raise err
149
+ else
150
+ res_1
151
+ end
152
+ end
153
+
154
+ def equivalent_schema_members?(member1, member2)
155
+ if member1.class != member2.class
156
+ return false
157
+ end
158
+
159
+ case member1
160
+ when Set
161
+ member1_array = member1.to_a.sort_by(&:graphql_name)
162
+ member2_array = member2.to_a.sort_by(&:graphql_name)
163
+ member1_array.each_with_index do |inner_member1, idx|
164
+ inner_member2 = member2_array[idx]
165
+ equivalent_schema_members?(inner_member1, inner_member2)
166
+ end
167
+ when GraphQL::Schema::Field
168
+ if member1.introspection? && member2.introspection?
169
+ member1.inspect == member2.inspect
170
+ else
171
+ member1 == member2
172
+ end
173
+ when Module
174
+ if member1.introspection? && member2.introspection?
175
+ member1.graphql_name == member2.graphql_name
176
+ else
177
+ member1 == member2
178
+ end
179
+ else
180
+ member1 == member2
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end