graphql 2.4.1 → 2.4.4

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12a25d94ae9348527390bad889be7c918390c0a1b133735612ac5a80ca0c6633
4
- data.tar.gz: de4bb61a31dc643d359157dff498410f70a5a7bd18d6c254463a2a5294706949
3
+ metadata.gz: caa82217695402eca9cd3782abf5deb4b27dd61d8aa3c16e7c4a555e1c09634a
4
+ data.tar.gz: a39b5821df1a8656616fd1831878c365c930289c0f94d733b788ab4298f24a96
5
5
  SHA512:
6
- metadata.gz: f85c5a5e4ab0083862c990fe6cd3f0ed99d36a414721d4c3d0a7a3e5c59042cdc5611a6b74d10d0e18525d9e51a32dc6aa1a2349c789b04dae75a835f4bb322a
7
- data.tar.gz: e0304dded75753771417672c704054a1daec13ab428ed92f97dbdcee2771f21874bfb6aedf9bd0054806ed97b4483b98376f03526b129d96628735c1933f0d35
6
+ metadata.gz: 1181acff2ccf066def91b1f39644b449fe715d3cfa18437c7edf74dfa5af01a45cbd1e00db2413810917fc290d1d3abc8bcc0620354458721d4df184973beb17
7
+ data.tar.gz: 61bcb56a34d0b0beb61672e3bae868dc5bd7a9caf351d85e0a78afd16c12347a076767e63ce55cc468c6edc5247caa10e2ccace32447b54c9dbc4f864846db5b
@@ -56,7 +56,14 @@ module GraphQL
56
56
  else
57
57
  @arguments = if @field
58
58
  @query.after_lazy(@query.arguments_for(@ast_nodes.first, @field)) do |args|
59
- args.is_a?(Execution::Interpreter::Arguments) ? args.keyword_arguments : args
59
+ case args
60
+ when Execution::Interpreter::Arguments
61
+ args.keyword_arguments
62
+ when GraphQL::ExecutionError
63
+ EmptyObjects::EMPTY_HASH
64
+ else
65
+ args
66
+ end
60
67
  end
61
68
  else
62
69
  nil
@@ -38,38 +38,17 @@ module GraphQL
38
38
  @all_types = {}
39
39
  @all_types_loaded = false
40
40
  @unvisited_types = []
41
- @referenced_types = Hash.new { |h, type_defn| h[type_defn] = [] }.compare_by_identity
42
- @cached_directives = {}
43
41
  @all_directives = nil
44
- @cached_visible = Hash.new { |h, member|
45
- h[member] = @schema.visible?(member, @context)
46
- }.compare_by_identity
42
+ @cached_visible = Hash.new { |h, member| h[member] = @schema.visible?(member, @context) }.compare_by_identity
47
43
 
48
44
  @cached_visible_fields = Hash.new { |h, owner|
49
45
  h[owner] = Hash.new do |h2, field|
50
- h2[field] = if @cached_visible[field] &&
51
- (ret_type = field.type.unwrap) &&
52
- @cached_visible[ret_type] &&
53
- (owner == field.owner || (!owner.kind.object?) || field_on_visible_interface?(field, owner))
54
-
55
- if !field.introspection?
56
- # The problem is that some introspection fields may have references
57
- # to non-custom introspection types.
58
- # If those were added here, they'd cause a DuplicateNamesError.
59
- # This is basically a bug -- those fields _should_ reference the custom types.
60
- add_type(ret_type, field)
61
- end
62
- true
63
- else
64
- false
65
- end
46
+ h2[field] = visible_field_for(owner, field)
66
47
  end.compare_by_identity
67
48
  }.compare_by_identity
68
49
 
69
50
  @cached_visible_arguments = Hash.new do |h, arg|
70
51
  h[arg] = if @cached_visible[arg] && (arg_type = arg.type.unwrap) && @cached_visible[arg_type]
71
- add_type(arg_type, arg)
72
- arg.validate_default_value
73
52
  true
74
53
  else
75
54
  false
@@ -88,39 +67,7 @@ module GraphQL
88
67
  end
89
68
  end.compare_by_identity
90
69
 
91
- @cached_possible_types = Hash.new do |h, type|
92
- h[type] = case type.kind.name
93
- when "INTERFACE"
94
- load_all_types
95
- pts = []
96
- @unfiltered_interface_type_memberships[type].each { |itm|
97
- if @cached_visible[itm] && (ot = itm.object_type) && @cached_visible[ot] && referenced?(ot)
98
- pts << ot
99
- end
100
- }
101
- pts
102
- when "UNION"
103
- pts = []
104
- type.type_memberships.each { |tm|
105
- if @cached_visible[tm] &&
106
- (ot = tm.object_type) &&
107
- @cached_visible[ot] &&
108
- referenced?(ot)
109
- pts << ot
110
- end
111
- }
112
- pts
113
- when "OBJECT"
114
- load_all_types
115
- if @all_types[type.graphql_name] == type
116
- [type]
117
- else
118
- EmptyObjects::EMPTY_ARRAY
119
- end
120
- else
121
- GraphQL::EmptyObjects::EMPTY_ARRAY
122
- end
123
- end.compare_by_identity
70
+ @cached_possible_types = Hash.new { |h, type| h[type] = possible_types_for(type) }.compare_by_identity
124
71
 
125
72
  @cached_enum_values = Hash.new do |h, enum_t|
126
73
  values = non_duplicate_items(enum_t.enum_values(@context), @cached_visible)
@@ -164,17 +111,12 @@ module GraphQL
164
111
  end
165
112
 
166
113
  def type(type_name)
167
- t = if (loaded_t = @all_types[type_name])
168
- loaded_t
169
- elsif !@all_types_loaded
170
- load_all_types
171
- @all_types[type_name]
172
- end
114
+ t = @schema.visibility.get_type(type_name) # rubocop:disable ContextIsPassedCop
173
115
  if t
174
116
  if t.is_a?(Array)
175
117
  vis_t = nil
176
118
  t.each do |t_defn|
177
- if @cached_visible[t_defn]
119
+ if @cached_visible[t_defn] && referenced?(t_defn)
178
120
  if vis_t.nil?
179
121
  vis_t = t_defn
180
122
  else
@@ -184,7 +126,7 @@ module GraphQL
184
126
  end
185
127
  vis_t
186
128
  else
187
- if t && @cached_visible[t]
129
+ if t && @cached_visible[t] && referenced?(t)
188
130
  t
189
131
  else
190
132
  nil
@@ -267,15 +209,15 @@ module GraphQL
267
209
  end
268
210
 
269
211
  def query_root
270
- add_if_visible(@schema.query)
212
+ ((t = @schema.query) && @cached_visible[t]) ? t : nil
271
213
  end
272
214
 
273
215
  def mutation_root
274
- add_if_visible(@schema.mutation)
216
+ ((t = @schema.mutation) && @cached_visible[t]) ? t : nil
275
217
  end
276
218
 
277
219
  def subscription_root
278
- add_if_visible(@schema.subscription)
220
+ ((t = @schema.subscription) && @cached_visible[t]) ? t : nil
279
221
  end
280
222
 
281
223
  def all_types
@@ -293,28 +235,15 @@ module GraphQL
293
235
  end
294
236
 
295
237
  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
238
+ directives.any? { |d| d.graphql_name == dir_name }
302
239
  end
303
240
 
304
241
  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
242
+ @all_directives ||= @schema.visibility.all_directives.select { |dir| @cached_visible[dir] }
315
243
  end
316
244
 
317
245
  def loadable?(t, _ctx)
246
+ load_all_types
318
247
  !@all_types[t.graphql_name] && @cached_visible[t]
319
248
  end
320
249
 
@@ -322,9 +251,9 @@ module GraphQL
322
251
  @all_types.values
323
252
  end
324
253
 
325
- def reachable_type?(name)
254
+ def reachable_type?(type_name)
326
255
  load_all_types
327
- !!@all_types[name]
256
+ !!@all_types[type_name]
328
257
  end
329
258
 
330
259
  def visible_enum_value?(enum_value, _ctx = nil)
@@ -333,29 +262,6 @@ module GraphQL
333
262
 
334
263
  private
335
264
 
336
- def add_if_visible(t)
337
- (t && @cached_visible[t]) ? (add_type(t, true); t) : nil
338
- end
339
-
340
- def add_type(t, by_member)
341
- if t && @cached_visible[t]
342
- n = t.graphql_name
343
- if (prev_t = @all_types[n])
344
- if !prev_t.equal?(t)
345
- raise_duplicate_definition(prev_t, t)
346
- end
347
- false
348
- else
349
- @referenced_types[t] << by_member
350
- @all_types[n] = t
351
- @unvisited_types << t
352
- true
353
- end
354
- else
355
- false
356
- end
357
- end
358
-
359
265
  def non_duplicate_items(definitions, visibility_cache)
360
266
  non_dups = []
361
267
  definitions.each do |defn|
@@ -373,153 +279,71 @@ module GraphQL
373
279
  raise DuplicateNamesError.new(duplicated_name: first_defn.path, duplicated_definition_1: first_defn.inspect, duplicated_definition_2: second_defn.inspect)
374
280
  end
375
281
 
376
- def referenced?(t)
377
- load_all_types
378
- @referenced_types[t].any? { |reference| (reference == true) || @cached_visible[reference] }
379
- end
380
-
381
282
  def load_all_types
382
283
  return if @all_types_loaded
383
284
  @all_types_loaded = true
384
- entry_point_types = [
385
- query_root,
386
- mutation_root,
387
- subscription_root,
388
- *@schema.introspection_system.types.values,
389
- ]
390
-
391
- # Don't include any orphan_types whose interfaces aren't visible.
392
- @schema.orphan_types.each do |orphan_type|
393
- if @cached_visible[orphan_type] &&
394
- orphan_type.interface_type_memberships.any? { |tm| @cached_visible[tm] && @cached_visible[tm.abstract_type] }
395
- entry_point_types << orphan_type
396
- end
397
- end
398
-
399
- @schema.directives.each do |_dir_name, dir_class|
400
- if @cached_visible[dir_class]
401
- arguments(dir_class).each do |arg|
402
- entry_point_types << arg.type.unwrap
403
- end
404
- end
405
- end
406
-
407
- entry_point_types.compact! # TODO why is this necessary?!
408
- entry_point_types.flatten! # handle multiple defns
409
- entry_point_types.each { |t| add_type(t, true) }
410
-
411
- @unfiltered_interface_type_memberships = Hash.new { |h, k| h[k] = [] }.compare_by_identity
412
- @add_possible_types = Set.new
413
- @late_types = []
414
-
415
- while @unvisited_types.any? || @late_types.any?
416
- while t = @unvisited_types.pop
417
- # These have already been checked for `.visible?`
418
- visit_type(t)
419
- end
420
- @add_possible_types.each do |int_t|
421
- itms = @unfiltered_interface_type_memberships[int_t]
422
- itms.each do |itm|
423
- if @cached_visible[itm] && (obj_type = itm.object_type) && @cached_visible[obj_type]
424
- add_type(obj_type, itm)
285
+ visit = Visibility::Visit.new(@schema) do |member|
286
+ if member.is_a?(Module) && member.respond_to?(:kind)
287
+ if @cached_visible[member]
288
+ type_name = member.graphql_name
289
+ if (prev_t = @all_types[type_name]) && !prev_t.equal?(member)
290
+ raise_duplicate_definition(member, prev_t)
425
291
  end
292
+ @all_types[type_name] = member
293
+ true
294
+ else
295
+ false
426
296
  end
427
- end
428
- @add_possible_types.clear
429
-
430
- while (union_tm = @late_types.shift)
431
- late_obj_t = union_tm.object_type
432
- obj_t = @all_types[late_obj_t.graphql_name] || raise("Failed to resolve #{late_obj_t.graphql_name.inspect} from #{union_tm.inspect}")
433
- union_tm.abstract_type.assign_type_membership_object_type(obj_t)
297
+ else
298
+ @cached_visible[member]
434
299
  end
435
300
  end
436
-
301
+ visit.visit_each
437
302
  @all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) }
438
303
  nil
439
304
  end
440
305
 
441
- def visit_type(type)
442
- visit_directives(type)
443
- case type.kind.name
444
- when "OBJECT", "INTERFACE"
445
- if type.kind.object?
446
- type.interface_type_memberships.each do |itm|
447
- @unfiltered_interface_type_memberships[itm.abstract_type] << itm
448
- end
449
- # recurse into visible implemented interfaces
450
- interfaces(type).each do |interface|
451
- add_type(interface, type)
452
- end
453
- else
454
- type.orphan_types.each { |t| add_type(t, type)}
455
- end
456
-
457
- # recurse into visible fields
458
- t_f = type.all_field_definitions
459
- t_f.each do |field|
460
- field.ensure_loaded
461
- if @cached_visible[field]
462
- visit_directives(field)
463
- field_type = field.type.unwrap
464
- if field_type.kind.interface?
465
- @add_possible_types.add(field_type)
466
- end
467
- add_type(field_type, field)
306
+ def referenced?(type_defn)
307
+ @schema.visibility.all_references[type_defn].any? { |r| r == true || @cached_visible[r] }
308
+ end
468
309
 
469
- # recurse into visible arguments
470
- arguments(field).each do |argument|
471
- visit_directives(argument)
472
- add_type(argument.type.unwrap, argument)
473
- end
310
+ def possible_types_for(type)
311
+ case type.kind.name
312
+ when "INTERFACE"
313
+ pts = []
314
+ @schema.visibility.all_interface_type_memberships[type].each do |itm|
315
+ if @cached_visible[itm] && (ot = itm.object_type) && @cached_visible[ot] && referenced?(ot)
316
+ pts << ot
474
317
  end
475
318
  end
476
- when "INPUT_OBJECT"
477
- # recurse into visible arguments
478
- arguments(type).each do |argument|
479
- visit_directives(argument)
480
- add_type(argument.type.unwrap, argument)
481
- end
319
+ pts
482
320
  when "UNION"
483
- # recurse into visible possible types
484
- type.type_memberships.each do |tm|
485
- if @cached_visible[tm]
486
- obj_t = tm.object_type
487
- if obj_t.is_a?(GraphQL::Schema::LateBoundType)
488
- @late_types << tm
489
- else
490
- if obj_t.is_a?(String)
491
- obj_t = Member::BuildType.constantize(obj_t)
492
- tm.object_type = obj_t
493
- end
494
- if @cached_visible[obj_t]
495
- add_type(obj_t, tm)
496
- end
497
- end
321
+ pts = []
322
+ type.type_memberships.each { |tm|
323
+ if @cached_visible[tm] &&
324
+ (ot = tm.object_type) &&
325
+ @cached_visible[ot] &&
326
+ referenced?(ot)
327
+ pts << ot
498
328
  end
329
+ }
330
+ pts
331
+ when "OBJECT"
332
+ if @cached_visible[type]
333
+ [type]
334
+ else
335
+ EmptyObjects::EMPTY_ARRAY
499
336
  end
500
- when "ENUM"
501
- enum_values(type).each do |val|
502
- visit_directives(val)
503
- end
504
- when "SCALAR"
505
- # pass
337
+ else
338
+ GraphQL::EmptyObjects::EMPTY_ARRAY
506
339
  end
507
340
  end
508
341
 
509
- def visit_directives(member)
510
- member.directives.each { |dir|
511
- dir_class = dir.class
512
- if @cached_visible[dir_class]
513
- dir_name = dir_class.graphql_name
514
- if (existing_dir = @cached_directives[dir_name])
515
- if existing_dir != dir_class
516
- raise ArgumentError, "Two directives for `@#{dir_name}`: #{existing_dir}, #{dir.class}"
517
- end
518
- else
519
- @cached_directives[dir.graphql_name] = dir_class
520
- end
521
- end
522
- }
342
+ def visible_field_for(owner, field)
343
+ @cached_visible[field] &&
344
+ (ret_type = field.type.unwrap) &&
345
+ @cached_visible[ret_type] &&
346
+ (owner == field.owner || (!owner.kind.object?) || field_on_visible_interface?(field, owner))
523
347
  end
524
348
  end
525
349
  end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ class Schema
4
+ class Visibility
5
+ class Visit
6
+ def initialize(schema, &visit_block)
7
+ @schema = schema
8
+ @late_bound_types = nil
9
+ @unvisited_types = nil
10
+ # These accumulate between calls to prevent re-visiting the same types
11
+ @visited_types = Set.new.compare_by_identity
12
+ @visited_directives = Set.new.compare_by_identity
13
+ @visit_block = visit_block
14
+ end
15
+
16
+ def entry_point_types
17
+ ept = [
18
+ @schema.query,
19
+ @schema.mutation,
20
+ @schema.subscription,
21
+ *@schema.introspection_system.types.values,
22
+ *@schema.orphan_types,
23
+ ]
24
+ ept.compact!
25
+ ept
26
+ end
27
+
28
+ def entry_point_directives
29
+ @schema.directives.values
30
+ end
31
+
32
+ def visit_each(types: entry_point_types, directives: entry_point_directives)
33
+ @unvisited_types && raise("Can't re-enter `visit_each` on this Visit (another visit is already in progress)")
34
+ @unvisited_types = types
35
+ @late_bound_types = []
36
+ directives_to_visit = directives
37
+
38
+ while @unvisited_types.any? || @late_bound_types.any?
39
+ while (type = @unvisited_types.pop)
40
+ if @visited_types.add?(type) && @visit_block.call(type)
41
+ directives_to_visit.concat(type.directives)
42
+ case type.kind.name
43
+ when "OBJECT", "INTERFACE"
44
+ type.interface_type_memberships.each do |itm|
45
+ append_unvisited_type(type, itm.abstract_type)
46
+ end
47
+ if type.kind.interface?
48
+ type.orphan_types.each do |orphan_type|
49
+ append_unvisited_type(type, orphan_type)
50
+ end
51
+ end
52
+
53
+ type.all_field_definitions.each do |field|
54
+ field.ensure_loaded
55
+ if @visit_block.call(field)
56
+ directives_to_visit.concat(field.directives)
57
+ append_unvisited_type(type, field.type.unwrap)
58
+ field.all_argument_definitions.each do |argument|
59
+ if @visit_block.call(argument)
60
+ directives_to_visit.concat(argument.directives)
61
+ append_unvisited_type(field, argument.type.unwrap)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ when "INPUT_OBJECT"
67
+ type.all_argument_definitions.each do |argument|
68
+ if @visit_block.call(argument)
69
+ directives_to_visit.concat(argument.directives)
70
+ append_unvisited_type(type, argument.type.unwrap)
71
+ end
72
+ end
73
+ when "UNION"
74
+ type.type_memberships.each do |tm|
75
+ append_unvisited_type(type, tm.object_type)
76
+ end
77
+ when "ENUM"
78
+ type.all_enum_value_definitions.each do |val|
79
+ if @visit_block.call(val)
80
+ directives_to_visit.concat(val.directives)
81
+ end
82
+ end
83
+ when "SCALAR"
84
+ # pass -- nothing else to visit
85
+ else
86
+ raise "Invariant: unhandled type kind: #{type.kind.inspect}"
87
+ end
88
+ end
89
+ end
90
+
91
+ directives_to_visit.each do |dir|
92
+ dir_class = dir.is_a?(Class) ? dir : dir.class
93
+ if @visited_directives.add?(dir_class) && @visit_block.call(dir_class)
94
+ dir_class.all_argument_definitions.each do |arg_defn|
95
+ if @visit_block.call(arg_defn)
96
+ directives_to_visit.concat(arg_defn.directives)
97
+ append_unvisited_type(dir_class, arg_defn.type.unwrap)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ missed_late_types_streak = 0
104
+ while (owner, late_type = @late_bound_types.shift)
105
+ if (late_type.is_a?(String) && (type = Member::BuildType.constantize(type))) ||
106
+ (late_type.is_a?(LateBoundType) && (type = @visited_types.find { |t| t.graphql_name == late_type.graphql_name }))
107
+ missed_late_types_streak = 0 # might succeed next round
108
+ update_type_owner(owner, type)
109
+ append_unvisited_type(owner, type)
110
+ else
111
+ # Didn't find it -- keep trying
112
+ missed_late_types_streak += 1
113
+ @late_bound_types << [owner, late_type]
114
+ if missed_late_types_streak == @late_bound_types.size
115
+ raise UnresolvedLateBoundTypeError.new(type: late_type)
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ @unvisited_types = nil
122
+ nil
123
+ end
124
+
125
+ private
126
+
127
+ def append_unvisited_type(owner, type)
128
+ if type.is_a?(LateBoundType) || type.is_a?(String)
129
+ @late_bound_types << [owner, type]
130
+ else
131
+ @unvisited_types << type
132
+ end
133
+ end
134
+
135
+ def update_type_owner(owner, type)
136
+ case owner
137
+ when Module
138
+ if owner.kind.union?
139
+ owner.assign_type_membership_object_type(type)
140
+ elsif type.kind.interface?
141
+ new_interfaces = []
142
+ owner.interfaces.each do |int_t|
143
+ if int_t.is_a?(String) && int_t == type.graphql_name
144
+ new_interfaces << type
145
+ elsif int_t.is_a?(LateBoundType) && int_t.graphql_name == type.graphql_name
146
+ new_interfaces << type
147
+ else
148
+ # Don't re-add proper interface definitions,
149
+ # they were probably already added, maybe with options.
150
+ end
151
+ end
152
+ owner.implements(*new_interfaces)
153
+ new_interfaces.each do |int|
154
+ pt = @possible_types[int] ||= []
155
+ if !pt.include?(owner) && owner.is_a?(Class)
156
+ pt << owner
157
+ end
158
+ int.interfaces.each do |indirect_int|
159
+ if indirect_int.is_a?(LateBoundType) && (indirect_int_type = get_type(indirect_int.graphql_name)) # rubocop:disable ContextIsPassedCop
160
+ update_type_owner(owner, indirect_int_type)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ when GraphQL::Schema::Argument, GraphQL::Schema::Field
166
+ orig_type = owner.type
167
+ # Apply list/non-null wrapper as needed
168
+ if orig_type.respond_to?(:of_type)
169
+ transforms = []
170
+ while (orig_type.respond_to?(:of_type))
171
+ if orig_type.kind.non_null?
172
+ transforms << :to_non_null_type
173
+ elsif orig_type.kind.list?
174
+ transforms << :to_list_type
175
+ else
176
+ raise "Invariant: :of_type isn't non-null or list"
177
+ end
178
+ orig_type = orig_type.of_type
179
+ end
180
+ transforms.reverse_each { |t| type = type.public_send(t) }
181
+ end
182
+ owner.type = type
183
+ else
184
+ raise "Unexpected update: #{owner.inspect} #{type.inspect}"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "graphql/schema/visibility/profile"
3
3
  require "graphql/schema/visibility/migration"
4
+ require "graphql/schema/visibility/visit"
4
5
 
5
6
  module GraphQL
6
7
  class Schema
@@ -13,24 +14,115 @@ module GraphQL
13
14
  # @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
14
15
  def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails) ? Rails.env.production? : nil), migration_errors: false)
15
16
  schema.visibility = self.new(schema, dynamic: dynamic, preload: preload, profiles: profiles, migration_errors: migration_errors)
17
+ if preload
18
+ schema.visibility.preload
19
+ end
16
20
  end
17
21
 
18
22
  def initialize(schema, dynamic:, preload:, profiles:, migration_errors:)
19
23
  @schema = schema
20
24
  schema.use_visibility_profile = true
21
- if migration_errors
22
- schema.visibility_profile_class = Migration
25
+ schema.visibility_profile_class = if migration_errors
26
+ Visibility::Migration
27
+ else
28
+ Visibility::Profile
23
29
  end
30
+ @preload = preload
24
31
  @profiles = profiles
25
32
  @cached_profiles = {}
26
33
  @dynamic = dynamic
27
34
  @migration_errors = migration_errors
28
- if preload
29
- profiles.each do |profile_name, example_ctx|
30
- example_ctx[:visibility_profile] = profile_name
31
- prof = profile_for(example_ctx, profile_name)
32
- prof.all_types # force loading
33
- end
35
+ # Top-level type caches:
36
+ @visit = nil
37
+ @interface_type_memberships = nil
38
+ @directives = nil
39
+ @types = nil
40
+ @references = nil
41
+ @loaded_all = false
42
+ end
43
+
44
+ def all_directives
45
+ load_all
46
+ @directives
47
+ end
48
+
49
+ def all_interface_type_memberships
50
+ load_all
51
+ @interface_type_memberships
52
+ end
53
+
54
+ def all_references
55
+ load_all
56
+ @references
57
+ end
58
+
59
+ def get_type(type_name)
60
+ load_all
61
+ @types[type_name]
62
+ end
63
+
64
+ def preload?
65
+ @preload
66
+ end
67
+
68
+ def preload
69
+ # Traverse the schema now (and in the *_configured hooks below)
70
+ # To make sure things are loaded during boot
71
+ @preloaded_types = Set.new
72
+ types_to_visit = [
73
+ @schema.query,
74
+ @schema.mutation,
75
+ @schema.subscription,
76
+ *@schema.introspection_system.types.values,
77
+ *@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap },
78
+ *@schema.orphan_types,
79
+ ]
80
+ # Root types may have been nil:
81
+ types_to_visit.compact!
82
+ ensure_all_loaded(types_to_visit)
83
+ @profiles.each do |profile_name, example_ctx|
84
+ example_ctx[:visibility_profile] = profile_name
85
+ prof = profile_for(example_ctx, profile_name)
86
+ prof.all_types # force loading
87
+ end
88
+ end
89
+
90
+ # @api private
91
+ def query_configured(query_type)
92
+ if @preload
93
+ ensure_all_loaded([query_type])
94
+ end
95
+ end
96
+
97
+ # @api private
98
+ def mutation_configured(mutation_type)
99
+ if @preload
100
+ ensure_all_loaded([mutation_type])
101
+ end
102
+ end
103
+
104
+ # @api private
105
+ def subscription_configured(subscription_type)
106
+ if @preload
107
+ ensure_all_loaded([subscription_type])
108
+ end
109
+ end
110
+
111
+ # @api private
112
+ def orphan_types_configured(orphan_types)
113
+ if @preload
114
+ ensure_all_loaded(orphan_types)
115
+ end
116
+ end
117
+
118
+ # @api private
119
+ def introspection_system_configured(introspection_system)
120
+ if @preload
121
+ introspection_types = [
122
+ *@schema.introspection_system.types.values,
123
+ *@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap },
124
+ ]
125
+ ensure_all_loaded(introspection_types)
34
126
  end
35
127
  end
36
128
 
@@ -57,7 +149,11 @@ module GraphQL
57
149
  if @profiles.any?
58
150
  if visibility_profile.nil?
59
151
  if @dynamic
60
- @schema.visibility_profile_class.new(context: context, schema: @schema)
152
+ if context.is_a?(Query::NullContext)
153
+ top_level_profile
154
+ else
155
+ @schema.visibility_profile_class.new(context: context, schema: @schema)
156
+ end
61
157
  elsif @profiles.any?
62
158
  raise ArgumentError, "#{@schema} expects a visibility profile, but `visibility_profile:` wasn't passed. Provide a `visibility_profile:` value or add `dynamic: true` to your visibility configuration."
63
159
  end
@@ -66,10 +162,119 @@ module GraphQL
66
162
  else
67
163
  @cached_profiles[visibility_profile] ||= @schema.visibility_profile_class.new(name: visibility_profile, context: context, schema: @schema)
68
164
  end
165
+ elsif context.is_a?(Query::NullContext)
166
+ top_level_profile
69
167
  else
70
168
  @schema.visibility_profile_class.new(context: context, schema: @schema)
71
169
  end
72
170
  end
171
+
172
+ attr_reader :top_level
173
+
174
+ # @api private
175
+ attr_reader :unfiltered_interface_type_memberships
176
+
177
+ def top_level_profile(refresh: false)
178
+ if refresh
179
+ @top_level_profile = nil
180
+ end
181
+ @top_level_profile ||= @schema.visibility_profile_class.new(context: Query::NullContext.instance, schema: @schema)
182
+ end
183
+
184
+ private
185
+
186
+ def ensure_all_loaded(types_to_visit)
187
+ while (type = types_to_visit.shift)
188
+ if type.kind.fields? && @preloaded_types.add?(type)
189
+ type.all_field_definitions.each do |field_defn|
190
+ field_defn.ensure_loaded
191
+ types_to_visit << field_defn.type.unwrap
192
+ end
193
+ end
194
+ end
195
+ top_level_profile(refresh: true)
196
+ nil
197
+ end
198
+
199
+ def load_all(types: nil)
200
+ if @visit.nil?
201
+ # Set up the visit system
202
+ @interface_type_memberships = Hash.new { |h, interface_type| h[interface_type] = [] }.compare_by_identity
203
+ @directives = []
204
+ @types = {} # String => Module
205
+ @references = Hash.new { |h, member| h[member] = [] }.compare_by_identity
206
+ @unions_for_references = Set.new
207
+ @visit = Visibility::Visit.new(@schema) do |member|
208
+ if member.is_a?(Module)
209
+ type_name = member.graphql_name
210
+ if (prev_t = @types[type_name])
211
+ if prev_t.is_a?(Array)
212
+ prev_t << member
213
+ else
214
+ @types[type_name] = [member, prev_t]
215
+ end
216
+ else
217
+ @types[member.graphql_name] = member
218
+ end
219
+ if member < GraphQL::Schema::Directive
220
+ @directives << member
221
+ elsif member.respond_to?(:interface_type_memberships)
222
+ member.interface_type_memberships.each do |itm|
223
+ @references[itm.abstract_type] << member
224
+ @interface_type_memberships[itm.abstract_type] << itm
225
+ end
226
+ elsif member < GraphQL::Schema::Union
227
+ @unions_for_references << member
228
+ end
229
+ elsif member.is_a?(GraphQL::Schema::Argument)
230
+ member.validate_default_value
231
+ @references[member.type.unwrap] << member
232
+ elsif member.is_a?(GraphQL::Schema::Field)
233
+ @references[member.type.unwrap] << member
234
+ end
235
+ true
236
+ end
237
+
238
+ @schema.root_types.each do |t|
239
+ @references[t] << true
240
+ end
241
+
242
+ @schema.introspection_system.types.each_value do |t|
243
+ @references[t] << true
244
+ end
245
+ @visit.visit_each(types: []) # visit default directives
246
+ end
247
+
248
+ if types
249
+ @visit.visit_each(types: types, directives: [])
250
+ elsif @loaded_all == false
251
+ @loaded_all = true
252
+ @visit.visit_each
253
+ else
254
+ # already loaded all
255
+ return
256
+ end
257
+
258
+ # TODO: somehow don't iterate over all these,
259
+ # only the ones that may have been modified
260
+ @interface_type_memberships.each do |int_type, type_memberships|
261
+ referers = @references[int_type].select { |r| r.is_a?(GraphQL::Schema::Field) }
262
+ if referers.any?
263
+ type_memberships.each do |type_membership|
264
+ implementor_type = type_membership.object_type
265
+ # Add new items only:
266
+ @references[implementor_type] |= referers
267
+ end
268
+ end
269
+ end
270
+
271
+ @unions_for_references.each do |union_type|
272
+ refs = @references[union_type]
273
+ union_type.all_possible_types.each do |object_type|
274
+ @references[object_type] |= refs # Add new items
275
+ end
276
+ end
277
+ end
73
278
  end
74
279
  end
75
280
  end
@@ -445,7 +445,17 @@ module GraphQL
445
445
  dup_defn = new_query_object || yield
446
446
  raise GraphQL::Error, "Second definition of `query(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@query_object.inspect}"
447
447
  elsif use_visibility_profile?
448
- @query_object = block_given? ? lazy_load_block : new_query_object
448
+ if block_given?
449
+ if visibility.preload?
450
+ @query_object = lazy_load_block.call
451
+ self.visibility.query_configured(@query_object)
452
+ else
453
+ @query_object = lazy_load_block
454
+ end
455
+ else
456
+ @query_object = new_query_object
457
+ self.visibility.query_configured(@query_object)
458
+ end
449
459
  else
450
460
  @query_object = new_query_object || lazy_load_block.call
451
461
  add_type_and_traverse(@query_object, root: true)
@@ -453,6 +463,8 @@ module GraphQL
453
463
  nil
454
464
  elsif @query_object.is_a?(Proc)
455
465
  @query_object = @query_object.call
466
+ self.visibility&.query_configured(@query_object)
467
+ @query_object
456
468
  else
457
469
  @query_object || find_inherited_value(:query)
458
470
  end
@@ -472,7 +484,17 @@ module GraphQL
472
484
  dup_defn = new_mutation_object || yield
473
485
  raise GraphQL::Error, "Second definition of `mutation(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@mutation_object.inspect}"
474
486
  elsif use_visibility_profile?
475
- @mutation_object = block_given? ? lazy_load_block : new_mutation_object
487
+ if block_given?
488
+ if visibility.preload?
489
+ @mutation_object = lazy_load_block.call
490
+ self.visibility.mutation_configured(@mutation_object)
491
+ else
492
+ @mutation_object = lazy_load_block
493
+ end
494
+ else
495
+ @mutation_object = new_mutation_object
496
+ self.visibility.mutation_configured(@mutation_object)
497
+ end
476
498
  else
477
499
  @mutation_object = new_mutation_object || lazy_load_block.call
478
500
  add_type_and_traverse(@mutation_object, root: true)
@@ -480,6 +502,8 @@ module GraphQL
480
502
  nil
481
503
  elsif @mutation_object.is_a?(Proc)
482
504
  @mutation_object = @mutation_object.call
505
+ self.visibility&.mutation_configured(@mutation_object)
506
+ @mutation_object
483
507
  else
484
508
  @mutation_object || find_inherited_value(:mutation)
485
509
  end
@@ -499,7 +523,17 @@ module GraphQL
499
523
  dup_defn = new_subscription_object || yield
500
524
  raise GraphQL::Error, "Second definition of `subscription(...)` (#{dup_defn.inspect}) is invalid, already configured with #{@subscription_object.inspect}"
501
525
  elsif use_visibility_profile?
502
- @subscription_object = block_given? ? lazy_load_block : new_subscription_object
526
+ if block_given?
527
+ if visibility.preload?
528
+ @subscription_object = lazy_load_block.call
529
+ visibility.subscription_configured(@subscription_object)
530
+ else
531
+ @subscription_object = lazy_load_block
532
+ end
533
+ else
534
+ @subscription_object = new_subscription_object
535
+ self.visibility.subscription_configured(@subscription_object)
536
+ end
503
537
  add_subscription_extension_if_necessary
504
538
  else
505
539
  @subscription_object = new_subscription_object || lazy_load_block.call
@@ -510,6 +544,7 @@ module GraphQL
510
544
  elsif @subscription_object.is_a?(Proc)
511
545
  @subscription_object = @subscription_object.call
512
546
  add_subscription_extension_if_necessary
547
+ self.visibility.subscription_configured(@subscription_object)
513
548
  @subscription_object
514
549
  else
515
550
  @subscription_object || find_inherited_value(:subscription)
@@ -695,20 +730,27 @@ module GraphQL
695
730
  type.fields(context)
696
731
  end
697
732
 
733
+ # Pass a custom introspection module here to use it for this schema.
734
+ # @param new_introspection_namespace [Module] If given, use this module for custom introspection on the schema
735
+ # @return [Module, nil] The configured namespace, if there is one
698
736
  def introspection(new_introspection_namespace = nil)
699
737
  if new_introspection_namespace
700
738
  @introspection = new_introspection_namespace
701
739
  # reset this cached value:
702
740
  @introspection_system = nil
741
+ introspection_system
742
+ @introspection
703
743
  else
704
744
  @introspection || find_inherited_value(:introspection)
705
745
  end
706
746
  end
707
747
 
748
+ # @return [Schema::IntrospectionSystem] Based on {introspection}
708
749
  def introspection_system
709
750
  if !@introspection_system
710
751
  @introspection_system = Schema::IntrospectionSystem.new(self)
711
752
  @introspection_system.resolve_late_bindings
753
+ self.visibility&.introspection_system_configured(@introspection_system)
712
754
  end
713
755
  @introspection_system
714
756
  end
@@ -952,6 +994,13 @@ module GraphQL
952
994
  end
953
995
  end
954
996
 
997
+ # Tell the schema about these types so that they can be registered as implementations of interfaces in the schema.
998
+ #
999
+ # This method must be used when an object type is connected to the schema as an interface implementor but
1000
+ # not as a return type of a field. In that case, if the object type isn't registered here, GraphQL-Ruby won't be able to find it.
1001
+ #
1002
+ # @param new_orphan_types [Array<Class<GraphQL::Schema::Object>>] Object types to register as implementations of interfaces in the schema.
1003
+ # @return [Array<Class<GraphQL::Schema::Object>>] All previously-registered orphan types for this schema
955
1004
  def orphan_types(*new_orphan_types)
956
1005
  if new_orphan_types.any?
957
1006
  new_orphan_types = new_orphan_types.flatten
@@ -968,6 +1017,7 @@ module GraphQL
968
1017
  end
969
1018
  add_type_and_traverse(new_orphan_types, root: false) unless use_visibility_profile?
970
1019
  own_orphan_types.concat(new_orphan_types.flatten)
1020
+ self.visibility&.orphan_types_configured(new_orphan_types)
971
1021
  end
972
1022
 
973
1023
  inherited_ot = find_inherited_value(:orphan_types, nil)
@@ -14,7 +14,9 @@ module GraphQL
14
14
  node_name: parent_type.graphql_name
15
15
  ))
16
16
  else
17
- message = "Field '#{node.name}' doesn't exist on type '#{parent_type.graphql_name}'#{context.did_you_mean_suggestion(node.name, context.types.fields(parent_type).map(&:graphql_name))}"
17
+ possible_fields = possible_fields(context, parent_type)
18
+ suggestion = context.did_you_mean_suggestion(node.name, possible_fields)
19
+ message = "Field '#{node.name}' doesn't exist on type '#{parent_type.graphql_name}'#{suggestion}"
18
20
  add_error(GraphQL::StaticValidation::FieldsAreDefinedOnTypeError.new(
19
21
  message,
20
22
  nodes: node,
@@ -26,6 +28,13 @@ module GraphQL
26
28
  super
27
29
  end
28
30
  end
31
+
32
+ private
33
+
34
+ def possible_fields(context, parent_type)
35
+ return EmptyObjects::EMPTY_ARRAY if parent_type.kind.leaf?
36
+ context.types.fields(parent_type).map(&:graphql_name)
37
+ end
29
38
  end
30
39
  end
31
40
  end
@@ -25,7 +25,7 @@ module GraphQL
25
25
  def validate_field_selections(ast_node, resolved_type)
26
26
  msg = if resolved_type.nil?
27
27
  nil
28
- elsif resolved_type.kind.scalar? && ast_node.selections.any?
28
+ elsif ast_node.selections.any? && resolved_type.kind.leaf?
29
29
  selection_strs = ast_node.selections.map do |n|
30
30
  case n
31
31
  when GraphQL::Language::Nodes::InlineFragment
@@ -38,7 +38,7 @@ module GraphQL
38
38
  raise "Invariant: unexpected selection node: #{n}"
39
39
  end
40
40
  end
41
- "Selections can't be made on scalars (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
41
+ "Selections can't be made on #{resolved_type.kind.name.sub("_", " ").downcase}s (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
42
42
  elsif resolved_type.kind.fields? && ast_node.selections.empty?
43
43
  "Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
44
44
  else
@@ -29,6 +29,7 @@ module GraphQL
29
29
  @visitor = visitor_class.new(document, self)
30
30
  end
31
31
 
32
+ # TODO stop using def_delegators because of Array allocations
32
33
  def_delegators :@visitor,
33
34
  :path, :type_definition, :field_definition, :argument_definition,
34
35
  :parent_type_definition, :directive_definition, :object_types, :dependencies
@@ -92,7 +92,7 @@ module GraphQL
92
92
  end
93
93
  graphql_result
94
94
  else
95
- unfiltered_type = Schema::Visibility::Profile.null_profile(schema: schema, context: context).type(type_name)
95
+ unfiltered_type = schema.use_visibility_profile? ? schema.visibility.get_type(type_name) : schema.get_type(type_name) # rubocop:disable ContextIsPassedCop
96
96
  if unfiltered_type
97
97
  raise TypeNotVisibleError.new(type_name: type_name)
98
98
  else
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "2.4.1"
3
+ VERSION = "2.4.4"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
4
+ version: 2.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-04 00:00:00.000000000 Z
11
+ date: 2024-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -504,6 +504,7 @@ files:
504
504
  - lib/graphql/schema/visibility.rb
505
505
  - lib/graphql/schema/visibility/migration.rb
506
506
  - lib/graphql/schema/visibility/profile.rb
507
+ - lib/graphql/schema/visibility/visit.rb
507
508
  - lib/graphql/schema/warden.rb
508
509
  - lib/graphql/schema/wrapper.rb
509
510
  - lib/graphql/static_validation.rb