graphql 2.4.9 → 2.4.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/current.rb +5 -0
  3. data/lib/graphql/dataloader/active_record_association_source.rb +64 -0
  4. data/lib/graphql/dataloader/active_record_source.rb +26 -0
  5. data/lib/graphql/dataloader/async_dataloader.rb +17 -5
  6. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  7. data/lib/graphql/dataloader/source.rb +2 -2
  8. data/lib/graphql/dataloader.rb +37 -5
  9. data/lib/graphql/execution/interpreter/runtime.rb +25 -6
  10. data/lib/graphql/execution/interpreter.rb +9 -1
  11. data/lib/graphql/language/parser.rb +1 -1
  12. data/lib/graphql/query.rb +8 -4
  13. data/lib/graphql/schema/enum.rb +1 -1
  14. data/lib/graphql/schema/interface.rb +1 -0
  15. data/lib/graphql/schema/loader.rb +1 -0
  16. data/lib/graphql/schema/member/has_dataloader.rb +56 -0
  17. data/lib/graphql/schema/member.rb +1 -0
  18. data/lib/graphql/schema/object.rb +17 -8
  19. data/lib/graphql/schema/resolver.rb +1 -5
  20. data/lib/graphql/schema/visibility/profile.rb +4 -4
  21. data/lib/graphql/schema/visibility.rb +14 -9
  22. data/lib/graphql/schema.rb +4 -4
  23. data/lib/graphql/static_validation/validator.rb +6 -1
  24. data/lib/graphql/tracing/appoptics_trace.rb +1 -1
  25. data/lib/graphql/tracing/new_relic_trace.rb +138 -41
  26. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  27. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  28. data/lib/graphql/tracing/perfetto_trace.rb +726 -0
  29. data/lib/graphql/tracing/trace.rb +124 -0
  30. data/lib/graphql/tracing.rb +1 -0
  31. data/lib/graphql/version.rb +1 -1
  32. metadata +36 -3
  33. data/lib/graphql/schema/null_mask.rb +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 687da1733554d6abed2daad506c7f9d55d9126e373274572af5b255c3d7b4918
4
- data.tar.gz: c2d2f58175df62c91ab31c3216b970a0bc09d1752250fc8192d1faa958cf1fe9
3
+ metadata.gz: e8174a7314a4dc73aa720915044a1b068a39eccb981ca7553b7f894b60e94bdb
4
+ data.tar.gz: 2bfb0e581036b944d2bd0f42b9ae580d990a848506b6ea20c9192c71c46e009d
5
5
  SHA512:
6
- metadata.gz: f4001700112f8ed43e78b4c739daac90973f08035bef426d5611d91248dd60d4d0070f8fff7042ac57b0549ddac62ac1111262fe0a47c10a83cd3c7a9a0612b8
7
- data.tar.gz: a6a1aaacf19bba38d2e0ee55ac9cdfa0d46b915706557c2dbac533d8fc58319d7c4ff8509019789fb39c46d6c01767f6cd27a02917fc7a3a7bdc30fc81eca5e4
6
+ metadata.gz: 5277ca8f41431b3854bbf85e64aa6c045701c803c18c927d21e2a89cdc2130a1c6bcb7c0e166fb3c7fcbdaac553f1bbf83bc00043ce8efd27b93dd8810b206a3
7
+ data.tar.gz: 0564e407251fad40e9a3cec275a172980407b97659a8db85cb177ab2b03c5244b320661096eb6e49b3df87897b39c66f727312bb310516341d962013d5899d02
@@ -48,5 +48,10 @@ module GraphQL
48
48
  def self.dataloader_source_class
49
49
  Fiber[:__graphql_current_dataloader_source]&.class
50
50
  end
51
+
52
+ # @return [GraphQL::Dataloader::Source, nil] The currently-running source, if there is one
53
+ def self.dataloader_source
54
+ Fiber[:__graphql_current_dataloader_source]
55
+ end
51
56
  end
52
57
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require "graphql/dataloader/source"
3
+ require "graphql/dataloader/active_record_source"
4
+
5
+ module GraphQL
6
+ class Dataloader
7
+ class ActiveRecordAssociationSource < GraphQL::Dataloader::Source
8
+ RECORD_SOURCE_CLASS = ActiveRecordSource
9
+
10
+ def initialize(association, scope = nil)
11
+ @association = association
12
+ @scope = scope
13
+ end
14
+
15
+ def load(record)
16
+ if (assoc = record.association(@association)).loaded?
17
+ assoc.target
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def fetch(records)
24
+ record_classes = Set.new.compare_by_identity
25
+ associated_classes = Set.new.compare_by_identity
26
+ records.each do |record|
27
+ if record_classes.add?(record.class)
28
+ reflection = record.class.reflect_on_association(@association)
29
+ if !reflection.polymorphic? && reflection.klass
30
+ associated_classes.add(reflection.klass)
31
+ end
32
+ end
33
+ end
34
+
35
+ available_records = []
36
+ associated_classes.each do |assoc_class|
37
+ already_loaded_records = dataloader.with(RECORD_SOURCE_CLASS, assoc_class).results.values
38
+ available_records.concat(already_loaded_records)
39
+ end
40
+
41
+ ::ActiveRecord::Associations::Preloader.new(records: records, associations: @association, available_records: available_records, scope: @scope).call
42
+
43
+ loaded_associated_records = records.map { |r| r.public_send(@association) }
44
+ records_by_model = {}
45
+ loaded_associated_records.each do |record|
46
+ if record
47
+ updates = records_by_model[record.class] ||= {}
48
+ updates[record.id] = record
49
+ end
50
+ end
51
+
52
+ if @scope.nil?
53
+ # Don't cache records loaded via scope because they might have reduced `SELECT`s
54
+ # Could check .select_values here?
55
+ records_by_model.each do |model_class, updates|
56
+ dataloader.with(RECORD_SOURCE_CLASS, model_class).merge(updates)
57
+ end
58
+ end
59
+
60
+ loaded_associated_records
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require "graphql/dataloader/source"
3
+
4
+ module GraphQL
5
+ class Dataloader
6
+ class ActiveRecordSource < GraphQL::Dataloader::Source
7
+ def initialize(model_class, find_by: model_class.primary_key)
8
+ @model_class = model_class
9
+ @find_by = find_by
10
+ @type_for_column = @model_class.type_for_attribute(@find_by)
11
+ end
12
+
13
+ def load(requested_key)
14
+ casted_key = @type_for_column.cast(requested_key)
15
+ super(casted_key)
16
+ end
17
+
18
+ def fetch(record_ids)
19
+ records = @model_class.where(@find_by => record_ids)
20
+ record_lookup = {}
21
+ records.each { |r| record_lookup[r.public_send(@find_by)] = r }
22
+ record_ids.map { |id| record_lookup[id] }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,16 +2,20 @@
2
2
  module GraphQL
3
3
  class Dataloader
4
4
  class AsyncDataloader < Dataloader
5
- def yield
5
+ def yield(source = Fiber[:__graphql_current_dataloader_source])
6
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
7
+ trace&.dataloader_fiber_yield(source)
6
8
  if (condition = Fiber[:graphql_dataloader_next_tick])
7
9
  condition.wait
8
10
  else
9
11
  Fiber.yield
10
12
  end
13
+ trace&.dataloader_fiber_resume(source)
11
14
  nil
12
15
  end
13
16
 
14
17
  def run
18
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
15
19
  jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
16
20
  job_fibers = []
17
21
  next_job_fibers = []
@@ -20,11 +24,12 @@ module GraphQL
20
24
  first_pass = true
21
25
  sources_condition = Async::Condition.new
22
26
  manager = spawn_fiber do
27
+ trace&.begin_dataloader(self)
23
28
  while first_pass || !job_fibers.empty?
24
29
  first_pass = false
25
30
  fiber_vars = get_fiber_variables
26
31
 
27
- while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber)))
32
+ while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
28
33
  if f.alive?
29
34
  finished = run_fiber(f)
30
35
  if !finished
@@ -38,7 +43,7 @@ module GraphQL
38
43
  Sync do |root_task|
39
44
  set_fiber_variables(fiber_vars)
40
45
  while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
41
- while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition))))
46
+ while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace))))
42
47
  if task.alive?
43
48
  root_task.yield # give the source task a chance to run
44
49
  next_source_tasks << task
@@ -50,6 +55,7 @@ module GraphQL
50
55
  end
51
56
  end
52
57
  end
58
+ trace&.end_dataloader(self)
53
59
  end
54
60
 
55
61
  manager.resume
@@ -63,7 +69,7 @@ module GraphQL
63
69
 
64
70
  private
65
71
 
66
- def spawn_source_task(parent_task, condition)
72
+ def spawn_source_task(parent_task, condition, trace)
67
73
  pending_sources = nil
68
74
  @source_cache.each_value do |source_by_batch_params|
69
75
  source_by_batch_params.each_value do |source|
@@ -77,10 +83,16 @@ module GraphQL
77
83
  if pending_sources
78
84
  fiber_vars = get_fiber_variables
79
85
  parent_task.async do
86
+ trace&.dataloader_spawn_source_fiber(pending_sources)
80
87
  set_fiber_variables(fiber_vars)
81
88
  Fiber[:graphql_dataloader_next_tick] = condition
82
- pending_sources.each(&:run_pending_keys)
89
+ pending_sources.each do |s|
90
+ trace&.begin_dataloader_source(s)
91
+ s.run_pending_keys
92
+ trace&.end_dataloader_source(s)
93
+ end
83
94
  cleanup_fiber
95
+ trace&.dataloader_fiber_exit
84
96
  end
85
97
  end
86
98
  end
@@ -11,7 +11,7 @@ module GraphQL
11
11
  # executed synchronously.
12
12
  def run; end
13
13
  def run_isolated; yield; end
14
- def yield
14
+ def yield(_source)
15
15
  raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
16
16
  end
17
17
 
@@ -93,14 +93,14 @@ module GraphQL
93
93
  # Then run the batch and update the cache.
94
94
  # @return [void]
95
95
  def sync(pending_result_keys)
96
- @dataloader.yield
96
+ @dataloader.yield(self)
97
97
  iterations = 0
98
98
  while pending_result_keys.any? { |key| !@results.key?(key) }
99
99
  iterations += 1
100
100
  if iterations > MAX_ITERATIONS
101
101
  raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency#{@dataloader.fiber_limit ? " or `fiber_limit: #{@dataloader.fiber_limit}` is set too low" : ""}."
102
102
  end
103
- @dataloader.yield
103
+ @dataloader.yield(self)
104
104
  end
105
105
  nil
106
106
  end
@@ -4,6 +4,8 @@ require "graphql/dataloader/null_dataloader"
4
4
  require "graphql/dataloader/request"
5
5
  require "graphql/dataloader/request_all"
6
6
  require "graphql/dataloader/source"
7
+ require "graphql/dataloader/active_record_association_source"
8
+ require "graphql/dataloader/active_record_source"
7
9
 
8
10
  module GraphQL
9
11
  # This plugin supports Fiber-based concurrency, along with {GraphQL::Dataloader::Source}.
@@ -129,8 +131,11 @@ module GraphQL
129
131
  # Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
130
132
  #
131
133
  # @return [void]
132
- def yield
134
+ def yield(source = Fiber[:__graphql_current_dataloader_source])
135
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
136
+ trace&.dataloader_fiber_yield(source)
133
137
  Fiber.yield
138
+ trace&.dataloader_fiber_resume(source)
134
139
  nil
135
140
  end
136
141
 
@@ -184,6 +189,7 @@ module GraphQL
184
189
  end
185
190
 
186
191
  def run
192
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
187
193
  jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
188
194
  job_fibers = []
189
195
  next_job_fibers = []
@@ -191,10 +197,11 @@ module GraphQL
191
197
  next_source_fibers = []
192
198
  first_pass = true
193
199
  manager = spawn_fiber do
200
+ trace&.begin_dataloader(self)
194
201
  while first_pass || !job_fibers.empty?
195
202
  first_pass = false
196
203
 
197
- while (f = (job_fibers.shift || (((next_job_fibers.size + job_fibers.size) < jobs_fiber_limit) && spawn_job_fiber)))
204
+ while (f = (job_fibers.shift || (((next_job_fibers.size + job_fibers.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
198
205
  if f.alive?
199
206
  finished = run_fiber(f)
200
207
  if !finished
@@ -205,7 +212,7 @@ module GraphQL
205
212
  join_queues(job_fibers, next_job_fibers)
206
213
 
207
214
  while (!source_fibers.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) })
208
- while (f = source_fibers.shift || (((job_fibers.size + source_fibers.size + next_source_fibers.size + next_job_fibers.size) < total_fiber_limit) && spawn_source_fiber))
215
+ while (f = source_fibers.shift || (((job_fibers.size + source_fibers.size + next_source_fibers.size + next_job_fibers.size) < total_fiber_limit) && spawn_source_fiber(trace)))
209
216
  if f.alive?
210
217
  finished = run_fiber(f)
211
218
  if !finished
@@ -216,6 +223,8 @@ module GraphQL
216
223
  join_queues(source_fibers, next_source_fibers)
217
224
  end
218
225
  end
226
+
227
+ trace&.end_dataloader(self)
219
228
  end
220
229
 
221
230
  run_fiber(manager)
@@ -230,6 +239,7 @@ module GraphQL
230
239
  if !source_fibers.empty?
231
240
  raise "Invariant: source fibers should have exited but #{source_fibers.size} remained"
232
241
  end
242
+
233
243
  rescue UncaughtThrowError => e
234
244
  throw e.tag, e.value
235
245
  end
@@ -247,6 +257,22 @@ module GraphQL
247
257
  }
248
258
  end
249
259
 
260
+ # Pre-warm the Dataloader cache with ActiveRecord objects which were loaded elsewhere.
261
+ # These will be used by {Dataloader::ActiveRecordSource}, {Dataloader::ActiveRecordAssociationSource} and their helper
262
+ # methods, `dataload_record` and `dataload_association`.
263
+ # @param records [Array<ActiveRecord::Base>] Already-loaded records to warm the cache with
264
+ # @param index_by [Symbol] The attribute to use as the cache key. (Should match `find_by:` when using {ActiveRecordSource})
265
+ # @return [void]
266
+ def merge_records(records, index_by: :id)
267
+ records_by_class = Hash.new { |h, k| h[k] = {} }
268
+ records.each do |r|
269
+ records_by_class[r.class][r.public_send(index_by)] = r
270
+ end
271
+ records_by_class.each do |r_class, records|
272
+ with(ActiveRecordSource, r_class).merge(records)
273
+ end
274
+ end
275
+
250
276
  private
251
277
 
252
278
  def calculate_fiber_limit
@@ -266,17 +292,19 @@ module GraphQL
266
292
  new_queue.clear
267
293
  end
268
294
 
269
- def spawn_job_fiber
295
+ def spawn_job_fiber(trace)
270
296
  if !@pending_jobs.empty?
271
297
  spawn_fiber do
298
+ trace&.dataloader_spawn_execution_fiber(@pending_jobs)
272
299
  while job = @pending_jobs.shift
273
300
  job.call
274
301
  end
302
+ trace&.dataloader_fiber_exit
275
303
  end
276
304
  end
277
305
  end
278
306
 
279
- def spawn_source_fiber
307
+ def spawn_source_fiber(trace)
280
308
  pending_sources = nil
281
309
  @source_cache.each_value do |source_by_batch_params|
282
310
  source_by_batch_params.each_value do |source|
@@ -289,10 +317,14 @@ module GraphQL
289
317
 
290
318
  if pending_sources
291
319
  spawn_fiber do
320
+ trace&.dataloader_spawn_source_fiber(pending_sources)
292
321
  pending_sources.each do |source|
293
322
  Fiber[:__graphql_current_dataloader_source] = source
323
+ trace&.begin_dataloader_source(source)
294
324
  source.run_pending_keys
325
+ trace&.end_dataloader_source(source)
295
326
  end
327
+ trace&.dataloader_fiber_exit
296
328
  end
297
329
  end
298
330
  end
@@ -218,8 +218,10 @@ module GraphQL
218
218
  result_name, field_ast_nodes_or_ast_node, selections_result
219
219
  )
220
220
  finished_jobs += 1
221
- if target_result && finished_jobs == enqueued_jobs
222
- selections_result.merge_into(target_result)
221
+ if finished_jobs == enqueued_jobs
222
+ if target_result
223
+ selections_result.merge_into(target_result)
224
+ end
223
225
  end
224
226
  @dataloader.clear_cache
225
227
  }
@@ -229,8 +231,10 @@ module GraphQL
229
231
  result_name, field_ast_nodes_or_ast_node, selections_result
230
232
  )
231
233
  finished_jobs += 1
232
- if target_result && finished_jobs == enqueued_jobs
233
- selections_result.merge_into(target_result)
234
+ if finished_jobs == enqueued_jobs
235
+ if target_result
236
+ selections_result.merge_into(target_result)
237
+ end
234
238
  end
235
239
  }
236
240
  end
@@ -371,6 +375,7 @@ module GraphQL
371
375
  end
372
376
  # Actually call the field resolver and capture the result
373
377
  app_result = begin
378
+ @current_trace.begin_execute_field(field_defn, object, kwarg_arguments, query)
374
379
  @current_trace.execute_field(field: field_defn, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments) do
375
380
  field_defn.resolve(object, kwarg_arguments, context)
376
381
  end
@@ -383,6 +388,7 @@ module GraphQL
383
388
  ex_err
384
389
  end
385
390
  end
391
+ @current_trace.end_execute_field(field_defn, object, kwarg_arguments, query, app_result)
386
392
  after_lazy(app_result, field: field_defn, ast_node: ast_node, owner_object: object, arguments: resolved_arguments, result_name: result_name, result: selection_result, runtime_state: runtime_state) do |inner_result, runtime_state|
387
393
  owner_type = selection_result.graphql_result_type
388
394
  return_type = field_defn.type
@@ -391,6 +397,8 @@ module GraphQL
391
397
  was_scoped = runtime_state.was_authorized_by_scope_items
392
398
  runtime_state.was_authorized_by_scope_items = nil
393
399
  continue_field(continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, resolved_arguments, result_name, selection_result, was_scoped, runtime_state)
400
+ else
401
+ nil
394
402
  end
395
403
  end
396
404
  end
@@ -781,8 +789,10 @@ module GraphQL
781
789
  runtime_state.was_authorized_by_scope_items = was_authorized_by_scope_items
782
790
  # Wrap the execution of _this_ method with tracing,
783
791
  # but don't wrap the continuation below
792
+ result = nil
784
793
  inner_obj = begin
785
- if trace
794
+ result = if trace
795
+ @current_trace.begin_execute_field(field, owner_object, arguments, query)
786
796
  @current_trace.execute_field_lazy(field: field, query: query, object: owner_object, arguments: arguments, ast_node: ast_node) do
787
797
  schema.sync_lazy(lazy_obj)
788
798
  end
@@ -797,6 +807,10 @@ module GraphQL
797
807
  rescue GraphQL::ExecutionError => ex_err
798
808
  ex_err
799
809
  end
810
+ ensure
811
+ if trace
812
+ @current_trace.end_execute_field(field, owner_object, arguments, query, result)
813
+ end
800
814
  end
801
815
  yield(inner_obj, runtime_state)
802
816
  end
@@ -840,14 +854,19 @@ module GraphQL
840
854
  end
841
855
 
842
856
  def resolve_type(type, value)
857
+ @current_trace.begin_resolve_type(type, value, context)
843
858
  resolved_type, resolved_value = @current_trace.resolve_type(query: query, type: type, object: value) do
844
859
  query.resolve_type(type, value)
845
860
  end
861
+ @current_trace.end_resolve_type(type, value, context, resolved_type)
846
862
 
847
863
  if lazy?(resolved_type)
848
864
  GraphQL::Execution::Lazy.new do
865
+ @current_trace.begin_resolve_type(type, value, context)
849
866
  @current_trace.resolve_type_lazy(query: query, type: type, object: value) do
850
- schema.sync_lazy(resolved_type)
867
+ rt = schema.sync_lazy(resolved_type)
868
+ @current_trace.end_resolve_type(type, value, context, rt)
869
+ rt
851
870
  end
852
871
  end
853
872
  else
@@ -33,9 +33,12 @@ module GraphQL
33
33
  end
34
34
  end
35
35
 
36
+
36
37
  multiplex = Execution::Multiplex.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
37
38
  Fiber[:__graphql_current_multiplex] = multiplex
38
- multiplex.current_trace.execute_multiplex(multiplex: multiplex) do
39
+ trace = multiplex.current_trace
40
+ trace.begin_execute_multiplex(multiplex)
41
+ trace.execute_multiplex(multiplex: multiplex) do
39
42
  schema = multiplex.schema
40
43
  queries = multiplex.queries
41
44
  lazies_at_depth = Hash.new { |h, k| h[k] = [] }
@@ -44,7 +47,10 @@ module GraphQL
44
47
  multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity]
45
48
  end
46
49
 
50
+ trace.begin_analyze_multiplex(multiplex, multiplex_analyzers)
47
51
  schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)
52
+ trace.end_analyze_multiplex(multiplex, multiplex_analyzers)
53
+
48
54
  begin
49
55
  # Since this is basically the batching context,
50
56
  # share it for a whole multiplex
@@ -148,6 +154,8 @@ module GraphQL
148
154
  }
149
155
  end
150
156
  end
157
+ ensure
158
+ trace&.end_execute_multiplex(multiplex)
151
159
  end
152
160
  end
153
161
 
@@ -161,7 +161,7 @@ module GraphQL
161
161
  expect_token(:VAR_SIGN)
162
162
  var_name = parse_name
163
163
  expect_token(:COLON)
164
- var_type = self.type
164
+ var_type = self.type || raise_parse_error("Missing type definition for variable: $#{var_name}")
165
165
  default_value = if at?(:EQUALS)
166
166
  advance_token
167
167
  value
data/lib/graphql/query.rb CHANGED
@@ -97,21 +97,22 @@ module GraphQL
97
97
  # @param root_value [Object] the object used to resolve fields on the root type
98
98
  # @param max_depth [Numeric] the maximum number of nested selections allowed for this query (falls back to schema-level value)
99
99
  # @param max_complexity [Numeric] the maximum field complexity for this query (falls back to schema-level value)
100
- # @param visibility_profile [Symbol]
100
+ # @param visibility_profile [Symbol] Another way to assign `context[:visibility_profile]`
101
101
  def initialize(schema, query_string = nil, query: nil, document: nil, context: nil, variables: nil, validate: true, static_validator: nil, visibility_profile: nil, subscription_topic: nil, operation_name: nil, root_value: nil, max_depth: schema.max_depth, max_complexity: schema.max_complexity, warden: nil, use_visibility_profile: nil)
102
102
  # Even if `variables: nil` is passed, use an empty hash for simpler logic
103
103
  variables ||= {}
104
104
  @schema = schema
105
105
  @context = schema.context_class.new(query: self, values: context)
106
+ if visibility_profile
107
+ @context[:visibility_profile] ||= visibility_profile
108
+ end
106
109
 
107
110
  if use_visibility_profile.nil?
108
111
  use_visibility_profile = warden ? false : schema.use_visibility_profile?
109
112
  end
110
113
 
111
- @visibility_profile = visibility_profile
112
-
113
114
  if use_visibility_profile
114
- @visibility_profile = @schema.visibility.profile_for(@context, visibility_profile)
115
+ @visibility_profile = @schema.visibility.profile_for(@context)
115
116
  @warden = Schema::Warden::NullWarden.new(context: @context, schema: @schema)
116
117
  else
117
118
  @visibility_profile = nil
@@ -440,6 +441,7 @@ module GraphQL
440
441
  @warden ||= @schema.warden_class.new(schema: @schema, context: @context)
441
442
  parse_error = nil
442
443
  @document ||= begin
444
+ current_trace.begin_parse(query_string)
443
445
  if query_string
444
446
  GraphQL.parse(query_string, trace: self.current_trace, max_tokens: @schema.max_query_string_tokens)
445
447
  end
@@ -447,6 +449,8 @@ module GraphQL
447
449
  parse_error = err
448
450
  @schema.parse_error(err, @context)
449
451
  nil
452
+ ensure
453
+ current_trace.end_parse(query_string)
450
454
  end
451
455
 
452
456
  @fragments = {}
@@ -241,7 +241,7 @@ module GraphQL
241
241
  return
242
242
  end
243
243
 
244
- instance_eval("def #{value_method_name}; #{value.graphql_name.inspect}; end;")
244
+ instance_eval("def #{value_method_name}; #{value.graphql_name.inspect}; end;", __FILE__, __LINE__)
245
245
  end
246
246
  end
247
247
 
@@ -13,6 +13,7 @@ module GraphQL
13
13
  include GraphQL::Schema::Member::Scoped
14
14
  include GraphQL::Schema::Member::HasAstNode
15
15
  include GraphQL::Schema::Member::HasUnresolvedTypeError
16
+ include GraphQL::Schema::Member::HasDataloader
16
17
  include GraphQL::Schema::Member::HasDirectives
17
18
  include GraphQL::Schema::Member::HasInterfaces
18
19
 
@@ -108,6 +108,7 @@ module GraphQL
108
108
  enum_value["name"],
109
109
  description: enum_value["description"],
110
110
  deprecation_reason: enum_value["deprecationReason"],
111
+ value_method: respond_to?(enum_value["name"].downcase) ? false : nil
111
112
  )
112
113
  end
113
114
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Schema
5
+ class Member
6
+ module HasDataloader
7
+ # @return [GraphQL::Dataloader] The dataloader for the currently-running query
8
+ def dataloader
9
+ context.dataloader
10
+ end
11
+
12
+ # A shortcut method for loading a key from a source.
13
+ # Identical to `dataloader.with(source_class, *source_args).load(load_key)`
14
+ # @param source_class [Class<GraphQL::Dataloader::Source>]
15
+ # @param source_args [Array<Object>] Any extra parameters defined in `source_class`'s `initialize` method
16
+ # @param load_key [Object] The key to look up using `def fetch`
17
+ def dataload(source_class, *source_args, load_key)
18
+ dataloader.with(source_class, *source_args).load(load_key)
19
+ end
20
+
21
+ # Find an object with ActiveRecord via {Dataloader::ActiveRecordSource}.
22
+ # @param model [Class<ActiveRecord::Base>]
23
+ # @param find_by_value [Object] Usually an `id`, might be another value if `find_by:` is also provided
24
+ # @param find_by [Symbol, String] A column name to look the record up by. (Defaults to the model's primary key.)
25
+ # @return [ActiveRecord::Base, nil]
26
+ def dataload_record(model, find_by_value, find_by: nil)
27
+ source = if find_by
28
+ dataloader.with(Dataloader::ActiveRecordSource, model, find_by: find_by)
29
+ else
30
+ dataloader.with(Dataloader::ActiveRecordSource, model)
31
+ end
32
+
33
+ source.load(find_by_value)
34
+ end
35
+
36
+ # Look up an associated record using a Rails association.
37
+ # @param association_name [Symbol] A `belongs_to` or `has_one` association. (If a `has_many` association is named here, it will be selected without pagination.)
38
+ # @param record [ActiveRecord::Base] The object that the association belongs to.
39
+ # @param scope [ActiveRecord::Relation] A scope to look up the associated record in
40
+ # @return [ActiveRecord::Base, nil] The associated record, if there is one
41
+ # @example Looking up a belongs_to on the current object
42
+ # dataload_association(:parent) # Equivalent to `object.parent`, but dataloaded
43
+ # @example Looking up an associated record on some other object
44
+ # dataload_association(:post, comment) # Equivalent to `comment.post`, but dataloaded
45
+ def dataload_association(record = object, association_name, scope: nil)
46
+ source = if scope
47
+ dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name, scope)
48
+ else
49
+ dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name)
50
+ end
51
+ source.load(record)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -2,6 +2,7 @@
2
2
  require 'graphql/schema/member/base_dsl_methods'
3
3
  require 'graphql/schema/member/graphql_type_names'
4
4
  require 'graphql/schema/member/has_ast_node'
5
+ require 'graphql/schema/member/has_dataloader'
5
6
  require 'graphql/schema/member/has_directives'
6
7
  require 'graphql/schema/member/has_deprecation_reason'
7
8
  require 'graphql/schema/member/has_interfaces'
@@ -7,6 +7,7 @@ module GraphQL
7
7
  class Object < GraphQL::Schema::Member
8
8
  extend GraphQL::Schema::Member::HasFields
9
9
  extend GraphQL::Schema::Member::HasInterfaces
10
+ include Member::HasDataloader
10
11
 
11
12
  # Raised when an Object doesn't have any field defined and hasn't explicitly opted out of this requirement
12
13
  class FieldsAreRequiredError < GraphQL::Error
@@ -65,20 +66,28 @@ module GraphQL
65
66
  # @return [GraphQL::Schema::Object, GraphQL::Execution::Lazy]
66
67
  # @raise [GraphQL::UnauthorizedError] if the user-provided hook returns `false`
67
68
  def authorized_new(object, context)
68
- maybe_lazy_auth_val = context.query.current_trace.authorized(query: context.query, type: self, object: object) do
69
- begin
70
- authorized?(object, context)
71
- rescue GraphQL::UnauthorizedError => err
72
- context.schema.unauthorized_object(err)
73
- rescue StandardError => err
74
- context.query.handle_or_reraise(err)
69
+ context.query.current_trace.begin_authorized(self, object, context)
70
+ begin
71
+ maybe_lazy_auth_val = context.query.current_trace.authorized(query: context.query, type: self, object: object) do
72
+ begin
73
+ authorized?(object, context)
74
+ rescue GraphQL::UnauthorizedError => err
75
+ context.schema.unauthorized_object(err)
76
+ rescue StandardError => err
77
+ context.query.handle_or_reraise(err)
78
+ end
75
79
  end
80
+ ensure
81
+ context.query.current_trace.end_authorized(self, object, context, maybe_lazy_auth_val)
76
82
  end
77
83
 
78
84
  auth_val = if context.schema.lazy?(maybe_lazy_auth_val)
79
85
  GraphQL::Execution::Lazy.new do
86
+ context.query.current_trace.begin_authorized(self, object, context)
80
87
  context.query.current_trace.authorized_lazy(query: context.query, type: self, object: object) do
81
- context.schema.sync_lazy(maybe_lazy_auth_val)
88
+ res = context.schema.sync_lazy(maybe_lazy_auth_val)
89
+ context.query.current_trace.end_authorized(self, object, context, res)
90
+ res
82
91
  end
83
92
  end
84
93
  else