Skip to content

Commit d1a6eb8

Browse files
authored
Merge pull request #1 from onrunning/features/1.10-ready
Features/1.10 ready
2 parents 01b55b7 + 6678d7f commit d1a6eb8

File tree

12 files changed

+452
-111
lines changed

12 files changed

+452
-111
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
/pkg/
88
/spec/reports/
99
/tmp/
10+
*.db

.ruby-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.1.5
1+
2.5.8

graphql-preload.gemspec

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ Gem::Specification.new do |spec|
2222
spec.require_paths = ['lib']
2323

2424
spec.add_runtime_dependency 'activerecord', '>= 4.1', '< 6'
25-
spec.add_runtime_dependency 'graphql', '>= 1.8', '< 2'
26-
spec.add_runtime_dependency 'graphql-batch', '~> 0.3'
25+
spec.add_runtime_dependency 'graphql', '>= 1.9', '< 2'
26+
spec.add_runtime_dependency 'graphql-batch', '~> 0.4'
2727
spec.add_runtime_dependency 'promise.rb', '~> 0.7'
2828

29-
spec.add_development_dependency 'bundler', '~> 1.16'
29+
spec.add_development_dependency 'bundler', '~> 2.1'
3030
spec.add_development_dependency 'minitest', '~> 5.0'
31-
spec.add_development_dependency 'pry', '~> 0.10'
31+
spec.add_development_dependency 'pry-byebug', '~> 3.7'
3232
spec.add_development_dependency 'rake', '~> 10.0'
33+
spec.add_development_dependency 'sqlite3', '~> 1.4'
3334
spec.add_development_dependency 'yard', '~> 0.9'
3435
end

lib/graphql/preload.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
require 'promise.rb'
44

55
GraphQL::Field.accepts_definitions(
6-
preload: ->(type, *args) do
6+
preload: lambda do |type, *args|
77
type.metadata[:preload] ||= []
88
type.metadata[:preload].concat(args)
99
end,
1010
preload_scope: ->(type, arg) { type.metadata[:preload_scope] = arg }
1111
)
1212

1313
GraphQL::Schema.accepts_definitions(
14-
enable_preloading: ->(schema) do
14+
enable_preloading: lambda do |schema|
1515
schema.instrument(:field, GraphQL::Preload::Instrument.new)
1616
end
1717
)
@@ -20,6 +20,7 @@ module GraphQL
2020
# Provides a GraphQL::Field definition to preload ActiveRecord::Associations
2121
module Preload
2222
autoload :Instrument, 'graphql/preload/instrument'
23+
autoload :FieldExtension, 'graphql/preload/field_extension'
2324
autoload :Loader, 'graphql/preload/loader'
2425
autoload :VERSION, 'graphql/preload/version'
2526

@@ -30,21 +31,24 @@ def enable_preloading
3031
end
3132

3233
module FieldMetadata
34+
attr_reader :preload
35+
attr_reader :preload_scope
36+
3337
def initialize(*args, preload: nil, preload_scope: nil, **kwargs, &block)
3438
if preload
3539
@preload ||= []
3640
@preload.concat Array.wrap preload
3741
end
38-
if preload_scope
39-
@preload_scope = preload_scope
40-
end
42+
43+
@preload_scope = preload_scope if preload_scope
44+
4145
super(*args, **kwargs, &block)
4246
end
4347

4448
def to_graphql
4549
field_defn = super
46-
field_defn.metadata[:preload] = @preload
47-
field_defn.metadata[:preload_scope] = @preload_scope
50+
field_defn.metadata[:preload] = @preload if @preload
51+
field_defn.metadata[:preload_scope] = @preload_scope if @preload_scope
4852
field_defn
4953
end
5054
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require 'graphql/preload/field_preloader'
2+
3+
module GraphQL
4+
module Preload
5+
class FieldExtension < GraphQL::Schema::FieldExtension
6+
include FieldPreloader
7+
8+
def resolve(object:, arguments:, context:)
9+
yield(object, arguments) unless object
10+
11+
scope = field.preload_scope.call(arguments, context) if field.preload_scope
12+
13+
preload(object.object, options, scope).then do
14+
yield(object, arguments)
15+
end
16+
end
17+
end
18+
end
19+
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
module GraphQL
2+
module Preload
3+
module FieldPreloader
4+
private
5+
6+
def preload(record, associations, scope)
7+
if associations.is_a?(String)
8+
raise TypeError, "Expected #{associations} to be a Symbol, not a String"
9+
elsif associations.is_a?(Symbol)
10+
return preload_single_association(record, associations, scope)
11+
end
12+
13+
promises = []
14+
15+
Array.wrap(associations).each do |association|
16+
case association
17+
when Symbol
18+
promises << preload_single_association(record, association, scope)
19+
when Array
20+
association.each do |sub_association|
21+
promises << preload(record, sub_association, scope)
22+
end
23+
when Hash
24+
association.each do |sub_association, nested_association|
25+
promises << preload_single_association(record, sub_association, scope).then do
26+
associated_records = record.public_send(sub_association)
27+
28+
case associated_records
29+
when ActiveRecord::Base
30+
preload(associated_records, nested_association, scope)
31+
else
32+
Promise.all(
33+
Array.wrap(associated_records).map do |associated_record|
34+
preload(associated_record, nested_association, scope)
35+
end
36+
)
37+
end
38+
end
39+
end
40+
end
41+
end
42+
43+
Promise.all(promises)
44+
end
45+
46+
def preload_single_association(record, association, scope)
47+
# We would like to pass the `scope` (which is an `ActiveRecord::Relation`),
48+
# directly into `Loader.for`. However, because the scope is
49+
# created for each parent record, they are different objects and
50+
# return different loaders, breaking batching.
51+
# Therefore, we pass in `scope.to_sql`, which is the same for all the
52+
# scopes and set the `scope` using an accessor. The actual scope
53+
# object used will be the last one, which shouldn't make any difference,
54+
# because even though they are different objects, they are all
55+
# functionally equivalent.
56+
loader = GraphQL::Preload::Loader.for(record.class, association, scope.try(:to_sql))
57+
loader.scope = scope
58+
loader.load(record)
59+
end
60+
end
61+
end
62+
end

lib/graphql/preload/instrument.rb

Lines changed: 18 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,33 @@
1+
require 'graphql/preload/field_preloader'
2+
13
module GraphQL
24
module Preload
3-
# Provides an instrument for the GraphQL::Field :preload definition
45
class Instrument
5-
def instrument(_type, field)
6-
metadata = merged_metadata(field)
7-
return field if metadata.fetch(:preload, nil).nil?
6+
include FieldPreloader
87

9-
old_resolver = field.resolve_proc
10-
new_resolver = ->(obj, args, ctx) do
11-
return old_resolver.call(obj, args, ctx) unless obj
12-
13-
if metadata[:preload_scope]
14-
scope = metadata[:preload_scope].call(args, ctx)
15-
end
16-
17-
is_graphql_object = obj.is_a?(GraphQL::Schema::Object)
18-
respond_to_object = obj.respond_to?(:object)
19-
record = is_graphql_object && respond_to_object ? obj.object : obj
20-
21-
preload(record, metadata[:preload], scope).then do
22-
old_resolver.call(obj, args, ctx)
23-
end
24-
end
25-
26-
field.redefine do
27-
resolve(new_resolver)
28-
end
29-
end
30-
31-
private def preload(record, associations, scope)
32-
if associations.is_a?(String)
33-
raise TypeError, "Expected #{associations} to be a Symbol, not a String"
34-
elsif associations.is_a?(Symbol)
35-
return preload_single_association(record, associations, scope)
36-
end
8+
def instrument(_type, field)
9+
return field unless field.metadata.include?(:preload)
3710

38-
promises = []
11+
if defined?(FieldExtension) && (type_class = field.metadata[:type_class])
12+
type_class.extension(FieldExtension)
13+
field
14+
else
15+
old_resolver = field.resolve_proc
16+
new_resolver = lambda do |obj, args, ctx|
17+
return old_resolver.call(obj, args, ctx) unless obj
3918

40-
Array.wrap(associations).each do |association|
41-
case association
42-
when Symbol
43-
promises << preload_single_association(record, association, scope)
44-
when Array
45-
association.each do |sub_association|
46-
promises << preload(record, sub_association, scope)
47-
end
48-
when Hash
49-
association.each do |sub_association, nested_association|
50-
promises << preload_single_association(record, sub_association, scope).then do
51-
associated_records = record.public_send(sub_association)
19+
scope = field.metadata[:preload_scope].call(args, ctx) if field.metadata[:preload_scope]
5220

53-
case associated_records
54-
when ActiveRecord::Base
55-
preload(associated_records, nested_association, scope)
56-
else
57-
Promise.all(
58-
Array.wrap(associated_records).map do |associated_record|
59-
preload(associated_record, nested_association, scope)
60-
end
61-
)
62-
end
63-
end
21+
preload(obj.object, field.metadata[:preload], scope).then do
22+
old_resolver.call(obj, args, ctx)
6423
end
6524
end
66-
end
67-
68-
Promise.all(promises)
69-
end
7025

71-
private def preload_single_association(record, association, scope)
72-
# We would like to pass the `scope` (which is an `ActiveRecord::Relation`),
73-
# directly into `Loader.for`. However, because the scope is
74-
# created for each parent record, they are different objects and
75-
# return different loaders, breaking batching.
76-
# Therefore, we pass in `scope.to_sql`, which is the same for all the
77-
# scopes and set the `scope` using an accessor. The actual scope
78-
# object used will be the last one, which shouldn't make any difference,
79-
# because even though they are different objects, they are all
80-
# functionally equivalent.
81-
loader = GraphQL::Preload::Loader.for(record.class, association, scope.try(:to_sql))
82-
loader.scope = scope
83-
loader.load(record)
84-
end
85-
86-
private def merged_metadata(field)
87-
type_class = field.metadata.fetch(:type_class, nil)
88-
89-
if type_class.nil? || !type_class.respond_to?(:to_graphql)
90-
field.metadata
91-
else
92-
field.metadata.merge(type_class.to_graphql.metadata)
26+
field.redefine do
27+
resolve(new_resolver)
28+
end
9329
end
9430
end
95-
9631
end
9732
end
9833
end

lib/graphql/preload/loader.rb

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def load(record)
2222
end
2323

2424
return Promise.resolve(record) if association_loaded?(record)
25+
2526
super
2627
end
2728

@@ -30,31 +31,31 @@ def perform(records)
3031
records.each { |record| fulfill(record, record) }
3132
end
3233

33-
private def association_loaded?(record)
34+
private
35+
36+
def association_loaded?(record)
3437
record.association(association).loaded?
3538
end
3639

37-
private def preload_association(records)
40+
def preload_association(records)
3841
ActiveRecord::Associations::Preloader.new.preload(records, association, preload_scope)
3942
end
4043

41-
private def preload_scope
44+
def preload_scope
4245
return nil unless scope
46+
4347
reflection = model.reflect_on_association(association)
4448
raise ArgumentError, 'Cannot specify preload_scope for polymorphic associations' if reflection.polymorphic?
49+
4550
scope if scope.try(:klass) == reflection.klass
4651
end
4752

48-
private def validate_association
49-
unless association.is_a?(Symbol)
50-
raise ArgumentError, 'Association must be a Symbol object'
51-
end
52-
53-
unless model < ActiveRecord::Base
54-
raise ArgumentError, 'Model must be an ActiveRecord::Base descendant'
55-
end
53+
def validate_association
54+
raise ArgumentError, 'Association must be a Symbol object' unless association.is_a?(Symbol)
55+
raise ArgumentError, "Model #{model} must be an ActiveRecord::Base descendant" unless model < ActiveRecord::Base
5656

5757
return if model.reflect_on_association(association)
58+
5859
raise TypeError, "Association :#{association} does not exist on #{model}"
5960
end
6061
end

0 commit comments

Comments
 (0)