graphql 2.3.14 → 2.3.15
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 +4 -4
- data/lib/generators/graphql/orm_mutations_base.rb +1 -1
- data/lib/generators/graphql/templates/base_resolver.erb +2 -0
- data/lib/generators/graphql/type_generator.rb +1 -1
- data/lib/graphql/analysis.rb +1 -1
- data/lib/graphql/execution/interpreter/resolve.rb +10 -6
- data/lib/graphql/language/comment.rb +18 -0
- data/lib/graphql/language/document_from_schema_definition.rb +38 -4
- data/lib/graphql/language/lexer.rb +15 -12
- data/lib/graphql/language/nodes.rb +22 -14
- data/lib/graphql/language/parser.rb +5 -0
- data/lib/graphql/language/printer.rb +23 -7
- data/lib/graphql/language.rb +6 -5
- data/lib/graphql/query.rb +1 -1
- data/lib/graphql/schema/argument.rb +13 -1
- data/lib/graphql/schema/enum.rb +1 -0
- data/lib/graphql/schema/enum_value.rb +9 -1
- data/lib/graphql/schema/field.rb +23 -3
- data/lib/graphql/schema/interface.rb +1 -0
- data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
- data/lib/graphql/schema/member/has_arguments.rb +2 -2
- data/lib/graphql/schema/member/has_fields.rb +2 -2
- data/lib/graphql/schema/resolver.rb +3 -4
- data/lib/graphql/schema/visibility/migration.rb +188 -0
- data/lib/graphql/schema/visibility/subset.rb +509 -0
- data/lib/graphql/schema/visibility.rb +30 -0
- data/lib/graphql/schema.rb +46 -41
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
- data/lib/graphql/testing/helpers.rb +1 -1
- data/lib/graphql/tracing/notifications_trace.rb +2 -2
- data/lib/graphql/version.rb +1 -1
- metadata +6 -4
- data/lib/graphql/schema/subset.rb +0 -509
- data/lib/graphql/schema/types_migration.rb +0 -187
@@ -1,509 +0,0 @@
|
|
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.each(&:ensure_loaded), @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.ensure_loaded
|
217
|
-
elsif f && @cached_visible_fields[owner][f.ensure_loaded]
|
218
|
-
f
|
219
|
-
else
|
220
|
-
nil
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
def fields(owner)
|
225
|
-
@cached_fields[owner]
|
226
|
-
end
|
227
|
-
|
228
|
-
def arguments(owner)
|
229
|
-
@cached_arguments[owner]
|
230
|
-
end
|
231
|
-
|
232
|
-
def argument(owner, arg_name)
|
233
|
-
arg = @cached_parent_arguments[owner][arg_name]
|
234
|
-
if arg.is_a?(Array)
|
235
|
-
visible_arg = nil
|
236
|
-
arg.each do |arg_defn|
|
237
|
-
if @cached_visible_arguments[arg_defn]
|
238
|
-
if visible_arg.nil?
|
239
|
-
visible_arg = arg_defn
|
240
|
-
else
|
241
|
-
raise_duplicate_definition(visible_arg, arg_defn)
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
visible_arg
|
246
|
-
else
|
247
|
-
if arg && @cached_visible_arguments[arg]
|
248
|
-
arg
|
249
|
-
else
|
250
|
-
nil
|
251
|
-
end
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def possible_types(type)
|
256
|
-
@cached_possible_types[type]
|
257
|
-
end
|
258
|
-
|
259
|
-
def interfaces(obj_or_int_type)
|
260
|
-
ints = obj_or_int_type.interface_type_memberships
|
261
|
-
.select { |itm| @cached_visible[itm] && @cached_visible[itm.abstract_type] }
|
262
|
-
.map!(&:abstract_type)
|
263
|
-
ints.uniq! # Remove any duplicate interfaces implemented via other interfaces
|
264
|
-
ints
|
265
|
-
end
|
266
|
-
|
267
|
-
def query_root
|
268
|
-
add_if_visible(@schema.query)
|
269
|
-
end
|
270
|
-
|
271
|
-
def mutation_root
|
272
|
-
add_if_visible(@schema.mutation)
|
273
|
-
end
|
274
|
-
|
275
|
-
def subscription_root
|
276
|
-
add_if_visible(@schema.subscription)
|
277
|
-
end
|
278
|
-
|
279
|
-
def all_types
|
280
|
-
load_all_types
|
281
|
-
@all_types.values
|
282
|
-
end
|
283
|
-
|
284
|
-
def all_types_h
|
285
|
-
load_all_types
|
286
|
-
@all_types
|
287
|
-
end
|
288
|
-
|
289
|
-
def enum_values(owner)
|
290
|
-
@cached_enum_values[owner]
|
291
|
-
end
|
292
|
-
|
293
|
-
def directive_exists?(dir_name)
|
294
|
-
if (dir = @schema.directives[dir_name]) && @cached_visible[dir]
|
295
|
-
!!dir
|
296
|
-
else
|
297
|
-
load_all_types
|
298
|
-
!!@cached_directives[dir_name]
|
299
|
-
end
|
300
|
-
end
|
301
|
-
|
302
|
-
def directives
|
303
|
-
@all_directives ||= begin
|
304
|
-
load_all_types
|
305
|
-
dirs = []
|
306
|
-
@schema.directives.each do |name, dir_defn|
|
307
|
-
if !@cached_directives[name] && @cached_visible[dir_defn]
|
308
|
-
dirs << dir_defn
|
309
|
-
end
|
310
|
-
end
|
311
|
-
dirs.concat(@cached_directives.values)
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
def loadable?(t, _ctx)
|
316
|
-
!@all_types[t.graphql_name] && @cached_visible[t]
|
317
|
-
end
|
318
|
-
|
319
|
-
def loaded_types
|
320
|
-
@all_types.values
|
321
|
-
end
|
322
|
-
|
323
|
-
def reachable_type?(name)
|
324
|
-
load_all_types
|
325
|
-
!!@all_types[name]
|
326
|
-
end
|
327
|
-
|
328
|
-
private
|
329
|
-
|
330
|
-
def add_if_visible(t)
|
331
|
-
(t && @cached_visible[t]) ? (add_type(t, true); t) : nil
|
332
|
-
end
|
333
|
-
|
334
|
-
def add_type(t, by_member)
|
335
|
-
if t && @cached_visible[t]
|
336
|
-
n = t.graphql_name
|
337
|
-
if (prev_t = @all_types[n])
|
338
|
-
if !prev_t.equal?(t)
|
339
|
-
raise_duplicate_definition(prev_t, t)
|
340
|
-
end
|
341
|
-
false
|
342
|
-
else
|
343
|
-
@referenced_types[t] << by_member
|
344
|
-
@all_types[n] = t
|
345
|
-
@unvisited_types << t
|
346
|
-
true
|
347
|
-
end
|
348
|
-
else
|
349
|
-
false
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
def non_duplicate_items(definitions, visibility_cache)
|
354
|
-
non_dups = []
|
355
|
-
definitions.each do |defn|
|
356
|
-
if visibility_cache[defn]
|
357
|
-
if (dup_defn = non_dups.find { |d| d.graphql_name == defn.graphql_name })
|
358
|
-
raise_duplicate_definition(dup_defn, defn)
|
359
|
-
end
|
360
|
-
non_dups << defn
|
361
|
-
end
|
362
|
-
end
|
363
|
-
non_dups
|
364
|
-
end
|
365
|
-
|
366
|
-
def raise_duplicate_definition(first_defn, second_defn)
|
367
|
-
raise DuplicateNamesError.new(duplicated_name: first_defn.path, duplicated_definition_1: first_defn.inspect, duplicated_definition_2: second_defn.inspect)
|
368
|
-
end
|
369
|
-
|
370
|
-
def referenced?(t)
|
371
|
-
load_all_types
|
372
|
-
@referenced_types[t].any? { |reference| (reference == true) || @cached_visible[reference] }
|
373
|
-
end
|
374
|
-
|
375
|
-
def load_all_types
|
376
|
-
return if @all_types_loaded
|
377
|
-
@all_types_loaded = true
|
378
|
-
entry_point_types = [
|
379
|
-
query_root,
|
380
|
-
mutation_root,
|
381
|
-
subscription_root,
|
382
|
-
*@schema.introspection_system.types.values,
|
383
|
-
]
|
384
|
-
|
385
|
-
# Don't include any orphan_types whose interfaces aren't visible.
|
386
|
-
@schema.orphan_types.each do |orphan_type|
|
387
|
-
if @cached_visible[orphan_type] &&
|
388
|
-
orphan_type.interface_type_memberships.any? { |tm| @cached_visible[tm] && @cached_visible[tm.abstract_type] }
|
389
|
-
entry_point_types << orphan_type
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
@schema.directives.each do |_dir_name, dir_class|
|
394
|
-
if @cached_visible[dir_class]
|
395
|
-
arguments(dir_class).each do |arg|
|
396
|
-
entry_point_types << arg.type.unwrap
|
397
|
-
end
|
398
|
-
end
|
399
|
-
end
|
400
|
-
|
401
|
-
entry_point_types.compact! # TODO why is this necessary?!
|
402
|
-
entry_point_types.flatten! # handle multiple defns
|
403
|
-
entry_point_types.each { |t| add_type(t, true) }
|
404
|
-
|
405
|
-
@unfiltered_interface_type_memberships = Hash.new { |h, k| h[k] = [] }.compare_by_identity
|
406
|
-
@add_possible_types = Set.new
|
407
|
-
|
408
|
-
while @unvisited_types.any?
|
409
|
-
while t = @unvisited_types.pop
|
410
|
-
# These have already been checked for `.visible?`
|
411
|
-
visit_type(t)
|
412
|
-
end
|
413
|
-
@add_possible_types.each do |int_t|
|
414
|
-
itms = @unfiltered_interface_type_memberships[int_t]
|
415
|
-
itms.each do |itm|
|
416
|
-
if @cached_visible[itm] && (obj_type = itm.object_type) && @cached_visible[obj_type]
|
417
|
-
add_type(obj_type, itm)
|
418
|
-
end
|
419
|
-
end
|
420
|
-
end
|
421
|
-
@add_possible_types.clear
|
422
|
-
end
|
423
|
-
|
424
|
-
@all_types.delete_if { |type_name, type_defn| !referenced?(type_defn) }
|
425
|
-
nil
|
426
|
-
end
|
427
|
-
|
428
|
-
def visit_type(type)
|
429
|
-
visit_directives(type)
|
430
|
-
case type.kind.name
|
431
|
-
when "OBJECT", "INTERFACE"
|
432
|
-
if type.kind.object?
|
433
|
-
type.interface_type_memberships.each do |itm|
|
434
|
-
@unfiltered_interface_type_memberships[itm.abstract_type] << itm
|
435
|
-
end
|
436
|
-
# recurse into visible implemented interfaces
|
437
|
-
interfaces(type).each do |interface|
|
438
|
-
add_type(interface, type)
|
439
|
-
end
|
440
|
-
else
|
441
|
-
type.orphan_types.each { |t| add_type(t, type)}
|
442
|
-
end
|
443
|
-
|
444
|
-
# recurse into visible fields
|
445
|
-
t_f = type.all_field_definitions
|
446
|
-
t_f.each do |field|
|
447
|
-
field.ensure_loaded
|
448
|
-
if @cached_visible[field]
|
449
|
-
visit_directives(field)
|
450
|
-
field_type = field.type.unwrap
|
451
|
-
if field_type.kind.interface?
|
452
|
-
@add_possible_types.add(field_type)
|
453
|
-
end
|
454
|
-
add_type(field_type, field)
|
455
|
-
|
456
|
-
# recurse into visible arguments
|
457
|
-
arguments(field).each do |argument|
|
458
|
-
visit_directives(argument)
|
459
|
-
add_type(argument.type.unwrap, argument)
|
460
|
-
end
|
461
|
-
end
|
462
|
-
end
|
463
|
-
when "INPUT_OBJECT"
|
464
|
-
# recurse into visible arguments
|
465
|
-
arguments(type).each do |argument|
|
466
|
-
visit_directives(argument)
|
467
|
-
add_type(argument.type.unwrap, argument)
|
468
|
-
end
|
469
|
-
when "UNION"
|
470
|
-
# recurse into visible possible types
|
471
|
-
type.type_memberships.each do |tm|
|
472
|
-
if @cached_visible[tm]
|
473
|
-
obj_t = tm.object_type
|
474
|
-
if obj_t.is_a?(String)
|
475
|
-
obj_t = Member::BuildType.constantize(obj_t)
|
476
|
-
tm.object_type = obj_t
|
477
|
-
end
|
478
|
-
if @cached_visible[obj_t]
|
479
|
-
add_type(obj_t, tm)
|
480
|
-
end
|
481
|
-
end
|
482
|
-
end
|
483
|
-
when "ENUM"
|
484
|
-
enum_values(type).each do |val|
|
485
|
-
visit_directives(val)
|
486
|
-
end
|
487
|
-
when "SCALAR"
|
488
|
-
# pass
|
489
|
-
end
|
490
|
-
end
|
491
|
-
|
492
|
-
def visit_directives(member)
|
493
|
-
member.directives.each { |dir|
|
494
|
-
dir_class = dir.class
|
495
|
-
if @cached_visible[dir_class]
|
496
|
-
dir_name = dir_class.graphql_name
|
497
|
-
if (existing_dir = @cached_directives[dir_name])
|
498
|
-
if existing_dir != dir_class
|
499
|
-
raise ArgumentError, "Two directives for `@#{dir_name}`: #{existing_dir}, #{dir.class}"
|
500
|
-
end
|
501
|
-
else
|
502
|
-
@cached_directives[dir.graphql_name] = dir_class
|
503
|
-
end
|
504
|
-
end
|
505
|
-
}
|
506
|
-
end
|
507
|
-
end
|
508
|
-
end
|
509
|
-
end
|
@@ -1,187 +0,0 @@
|
|
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
|
-
member1.ensure_loaded
|
169
|
-
member2.ensure_loaded
|
170
|
-
if member1.introspection? && member2.introspection?
|
171
|
-
member1.inspect == member2.inspect
|
172
|
-
else
|
173
|
-
member1 == member2
|
174
|
-
end
|
175
|
-
when Module
|
176
|
-
if member1.introspection? && member2.introspection?
|
177
|
-
member1.graphql_name == member2.graphql_name
|
178
|
-
else
|
179
|
-
member1 == member2
|
180
|
-
end
|
181
|
-
else
|
182
|
-
member1 == member2
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|