Skip to content

Commit

Permalink
Fixes #91. Applies scopes to eager-loaded associations when they are …
Browse files Browse the repository at this point in the history
…nested. (#92)
  • Loading branch information
asedge authored Mar 26, 2024
1 parent dc194f3 commit 7551972
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ def build_hash_includes(relation, model = current_sobject, parent_association_fi
private

def build_relation(association, nested_includes)
sub_query = Query.new(association.sfdc_association_field)
sub_query.fields association.relation_model.fields
builder_class = ActiveForce::Association::EagerLoadProjectionBuilder.projection_builder_class(association)
projection_builder = builder_class.new(association)
sub_query = projection_builder.query_with_association_fields
association_mapping[association.sfdc_association_field.downcase] = association.relation_name
nested_includes_query = self.class.build(nested_includes, association.relation_model)
sub_query.fields nested_includes_query[:fields]
Expand Down
29 changes: 18 additions & 11 deletions lib/active_force/association/eager_load_projection_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ class << self
def build(association, parent_association_field = nil)
new(association, parent_association_field).projections
end

def projection_builder_class(association)
klass = association.class.name.demodulize
ActiveForce::Association.const_get "#{klass}ProjectionBuilder"
rescue NameError
raise "No projection builder exists for #{klass}"
end
end

attr_reader :association, :parent_association_field
Expand All @@ -16,12 +23,10 @@ def initialize(association, parent_association_field = nil)
end

def projections
klass = association.class.name.split('::').last
builder_class = ActiveForce::Association.const_get "#{klass}ProjectionBuilder"
builder_class = self.class.projection_builder_class(association)
builder_class.new(association, parent_association_field).projections
rescue NameError
raise "Don't know how to build projections for #{klass}"
end

end

class AbstractProjectionBuilder
Expand All @@ -42,26 +47,28 @@ def apply_association_scope(query)

query.instance_exec(&association.scoped_as)
end
end

class HasManyAssociationProjectionBuilder < AbstractProjectionBuilder
###
# Use ActiveForce::Query to build a subquery for the SFDC
# relationship name. Per SFDC convention, the name needs
# to be pluralized
def projections
def query_with_association_fields
relationship_name = association.sfdc_association_field
query = ActiveQuery.new(association.relation_model, relationship_name)
query.fields association.relation_model.fields
["(#{apply_association_scope(query).to_s})"]
apply_association_scope(query)
end
end

class HasManyAssociationProjectionBuilder < AbstractProjectionBuilder
def projections
["(#{query_with_association_fields.to_s})"]
end
end

class HasOneAssociationProjectionBuilder < AbstractProjectionBuilder
def projections
query = ActiveQuery.new(association.relation_model, association.sfdc_association_field)
query.fields association.relation_model.fields
["(#{apply_association_scope(query).to_s})"]
["(#{query_with_association_fields.to_s})"]
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/active_force/association_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@
it 'allows passing a foreign key' do
Comment.belongs_to :post, foreign_key: :fancy_post_id
allow(comment).to receive(:fancy_post_id).and_return "2"
expect(client).to receive(:query).with("SELECT Id, Title__c, BlogId FROM Post__c WHERE (Id = '2') LIMIT 1")
expect(client).to receive(:query).with("SELECT Id, Title__c, BlogId, IsActive FROM Post__c WHERE (Id = '2') LIMIT 1")
comment.post
Comment.belongs_to :post # reset association to original value
end
Expand Down
50 changes: 41 additions & 9 deletions spec/active_force/sobject/includes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ module ActiveForce
context 'when assocation has a scope' do
it 'formulates the correct SOQL query with the scope applied' do
soql = Post.includes(:impossible_comments).where(id: '1234').to_s
expect(soql).to eq "SELECT Id, Title__c, BlogId, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comments__r WHERE (1 = 0)) FROM Post__c WHERE (Id = '1234')"
expect(soql).to eq "SELECT Id, Title__c, BlogId, IsActive, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comments__r WHERE (1 = 0)) FROM Post__c WHERE (Id = '1234')"
end
end

Expand Down Expand Up @@ -297,7 +297,7 @@ module ActiveForce
context 'when assocation has a scope' do
it 'formulates the correct SOQL query with the scope applied' do
soql = Post.includes(:last_comment).where(id: '1234').to_s
expect(soql).to eq "SELECT Id, Title__c, BlogId, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__r WHERE (NOT ((Body__c = NULL))) ORDER BY CreatedDate DESC) FROM Post__c WHERE (Id = '1234')"
expect(soql).to eq "SELECT Id, Title__c, BlogId, IsActive, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comment__r WHERE (NOT ((Body__c = NULL))) ORDER BY CreatedDate DESC) FROM Post__c WHERE (Id = '1234')"
end
end

Expand Down Expand Up @@ -397,9 +397,41 @@ module ActiveForce
end
end

context 'when the associations have scopes' do
it 'generates the correct SOQL query' do
soql = Blog.includes(active_posts: :impossible_comments).where(id: '123').to_s
expect(soql).to eq <<-SOQL.squish
SELECT Id, Name, Link__c,
(SELECT Id, Title__c, BlogId, IsActive,
(SELECT Id, PostId, PosterId__c, FancyPostId, Body__c
FROM Comments__r WHERE (1 = 0))
FROM Posts__r
WHERE (IsActive = true))
FROM Blog__c
WHERE (Id = '123')
SOQL
end

it 'builds the associated objects and caches them' do
response = [build_restforce_sobject({
'Id' => '123',
'Posts__r' => build_restforce_collection([
{'Id' => '213', 'IsActive' => true, 'Comments__r' => [{'Id' => '987'}]},
{'Id' => '214', 'IsActive' => true, 'Comments__r' => [{'Id' => '456'}]}
])
})]
allow(client).to receive(:query).once.and_return response
blog = Blog.includes(active_posts: :impossible_comments).find '123'
expect(blog.active_posts).to be_an Array
expect(blog.active_posts.all? { |o| o.is_a? Post }).to eq true
expect(blog.active_posts.first.impossible_comments.first).to be_a Comment
expect(blog.active_posts.first.impossible_comments.first.id).to eq '987'
end
end

context 'with namespaced sobjects' do
it 'formulates the correct SOQL query' do
soql = Salesforce::Account.includes({partner_opportunities: :owner}).where(id: '123').to_s
soql = Salesforce::Account.includes({opportunities: :owner}).where(id: '123').to_s
expect(soql).to eq <<-SOQL.squish
SELECT Id, Business_Partner__c,
(SELECT Id, OwnerId, AccountId, Business_Partner__c, Owner.Id
Expand All @@ -417,11 +449,11 @@ module ActiveForce
{'Id' => '214', 'AccountId' => '123', 'OwnerId' => '321', 'Business_Partner__c' => '123', 'Owner' => {'Id' => '321'}} ])
})]
allow(client).to receive(:query).once.and_return response
account = Salesforce::Account.includes({partner_opportunities: :owner}).find '123'
expect(account.partner_opportunities).to be_an Array
expect(account.partner_opportunities.all? { |o| o.is_a? Salesforce::Opportunity }).to eq true
expect(account.partner_opportunities.first.owner).to be_a Salesforce::User
expect(account.partner_opportunities.first.owner.id).to eq '321'
account = Salesforce::Account.includes({opportunities: :owner}).find '123'
expect(account.opportunities).to be_an Array
expect(account.opportunities.all? { |o| o.is_a? Salesforce::Opportunity }).to eq true
expect(account.opportunities.first.owner).to be_a Salesforce::User
expect(account.opportunities.first.owner.id).to eq '321'
end
end

Expand Down Expand Up @@ -631,7 +663,7 @@ module ActiveForce
soql = Comment.includes(post: :blog).where(id: '123').to_s
expect(soql).to eq <<-SOQL.squish
SELECT Id, PostId, PosterId__c, FancyPostId, Body__c,
PostId.Id, PostId.Title__c, PostId.BlogId,
PostId.Id, PostId.Title__c, PostId.BlogId, PostId.IsActive,
PostId.BlogId.Id, PostId.BlogId.Name, PostId.BlogId.Link__c
FROM Comment__c
WHERE (Id = '123')
Expand Down
3 changes: 3 additions & 0 deletions spec/support/sobjects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Post < ActiveForce::SObject
self.table_name = "Post__c"
field :title
field :blog_id, from: "BlogId"
field :is_active, from: "IsActive", as: :boolean
has_many :comments
has_many :impossible_comments, model: Comment, scoped_as: ->{ where('1 = 0') }
has_many :reply_comments, model: Comment, scoped_as: ->(post){ where(body: "RE: #{post.title}").order('CreationDate DESC') }
Expand All @@ -25,6 +26,7 @@ class Blog < ActiveForce::SObject
field :name, from: 'Name'
field :link, from: 'Link__c'
has_many :posts
has_many :active_posts, model: 'Post', scoped_as: -> { where(is_active: true) }
end
class Territory < ActiveForce::SObject
field :quota_id, from: "Quota__c"
Expand Down Expand Up @@ -149,6 +151,7 @@ class Opportunity < ActiveForce::SObject
end
class Account < ActiveForce::SObject
field :business_partner
has_many :opportunities, model: Opportunity
has_many :partner_opportunities, model: Opportunity, scoped_as: ->(account){ where(business_partner: account.business_partner).includes(:owner) }
end
end

0 comments on commit 7551972

Please sign in to comment.