active_force 0.15.1 → 0.16.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c325fe186d67cce9500a1df17360184bd97c2fa26fed2819b327f3ea8331df7c
4
- data.tar.gz: 482d5c2fabed72c3d12b646037dad7900f26e17260000a65e171d42ab41ce394
3
+ metadata.gz: 7691f9f6f82d893a6aafdf29cfc6228ab2fe31988fafcf5be95070347a0286de
4
+ data.tar.gz: 4d0da25e5e12f9014407dba1871953a0b05f58227188d53130eba27dd537aa4d
5
5
  SHA512:
6
- metadata.gz: 05a36678b1ba07b3e89b1a755e7526facd18cdef04cdfcb9dd1b08224f42bbcc779959c26e869a3ead38e5add649d710e4afc4f0c1f88cdbcd73287360da3f7a
7
- data.tar.gz: 13724331bdafe67729dafd0ca31a863657e125c8bb458fb8a0316aa5a2fba9f466c0e3ff52bc6fbc80a9190639636e726ac8c9940cc92836260b774cc7024ec5
6
+ metadata.gz: 2e8cf96c34eb81e3e4236455538bc462de07d02f7149ab064125d4c86f52a96c7e61e98ff91b9fe48e3b5b6d79d88ded4bb47966800f021e153a970adac802b4
7
+ data.tar.gz: 6b7edaa7cf4161921b1d42c3a26fe896781bd3198ce981b2693852f3fe3ff6f1690e27e61ec781761fbe06d10634fc720013f1b2f3a266d3de6a189dd37af2a5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Not released
4
4
 
5
+ ## 0.16.0
6
+
7
+ - Fix `default` in models when default value is overridden by the same value, it is still sent to salesforce (https://github.com/Beyond-Finance/active_force/pull/61)
8
+ - Support to fetch multi-level associations during eager load (https://github.com/Beyond-Finance/active_force/pull/62)
9
+
5
10
  ## 0.15.1
6
11
 
7
12
  - Revert new `pluck` implementation due to compatibility issues (https://github.com/Beyond-Finance/active_force/pull/60)
data/README.md CHANGED
@@ -192,6 +192,21 @@ It is also possible to eager load associations:
192
192
  Comment.includes(:post)
193
193
  ```
194
194
 
195
+ It is possible to eager load multi level associations
196
+
197
+ In order to utilize multi level eager loads, the API version should be set to 58.0 or higher when instantiating a Restforce client
198
+
199
+ ```ruby
200
+ Restforce.new({api_version: '58.0'})
201
+ ```
202
+
203
+ Examples:
204
+
205
+ ```ruby
206
+ Comment.includes(post: :owner)
207
+ Comment.includes({post: {owner: :account}})
208
+ ```
209
+
195
210
  ### Aggregates
196
211
 
197
212
  Summing the values of a column:
@@ -4,7 +4,6 @@ require 'forwardable'
4
4
 
5
5
  module ActiveForce
6
6
  class PreparedStatementInvalid < ArgumentError; end
7
-
8
7
  class RecordNotFound < StandardError
9
8
  attr_reader :table_name, :conditions
10
9
 
@@ -19,15 +18,16 @@ module ActiveForce
19
18
  class ActiveQuery < Query
20
19
  extend Forwardable
21
20
 
22
- attr_reader :sobject, :association_mapping
21
+ attr_reader :sobject, :association_mapping, :belongs_to_association_mapping
23
22
 
24
23
  def_delegators :sobject, :sfdc_client, :build, :table_name, :mappings
25
24
  def_delegators :to_a, :each, :map, :inspect, :pluck, :each_with_object
26
25
 
27
- def initialize sobject
26
+ def initialize (sobject, custom_table_name = nil)
28
27
  @sobject = sobject
29
28
  @association_mapping = {}
30
- super table_name
29
+ @belongs_to_association_mapping = {}
30
+ super custom_table_name || table_name
31
31
  fields sobject.fields
32
32
  end
33
33
 
@@ -93,12 +93,9 @@ module ActiveForce
93
93
  end
94
94
 
95
95
  def includes(*relations)
96
- relations.each do |relation|
97
- association = sobject.associations[relation]
98
- fields Association::EagerLoadProjectionBuilder.build association
99
- # downcase the key and downcase when we do the comparison so we don't do any more crazy string manipulation
100
- association_mapping[association.sfdc_association_field.downcase] = association.relation_name
101
- end
96
+ includes_query = Association::EagerLoadBuilderForNestedIncludes.build(relations, sobject)
97
+ fields includes_query[:fields]
98
+ association_mapping.merge!(includes_query[:association_mapping])
102
99
  self
103
100
  end
104
101
 
@@ -16,7 +16,11 @@ module ActiveForce
16
16
  end
17
17
 
18
18
  def default_relationship_name
19
- parent.mappings[foreign_key].gsub(/__c\z/, '__r')
19
+ if !parent.custom_table? && !relation_model.custom_table?
20
+ relation_model.table_name
21
+ else
22
+ parent.mappings[foreign_key].gsub(/__c\z/, '__r')
23
+ end
20
24
  end
21
25
 
22
26
  def default_foreign_key
@@ -0,0 +1,84 @@
1
+ module ActiveForce
2
+
3
+ module Association
4
+ class InvalidAssociationError < StandardError; end
5
+
6
+ class EagerLoadBuilderForNestedIncludes
7
+
8
+ class << self
9
+ def build(relations, current_sobject, parent_association_field = nil)
10
+ new(relations, current_sobject, parent_association_field).projections
11
+ end
12
+ end
13
+
14
+ attr_reader :relations, :current_sobject, :association_mapping, :parent_association_field, :fields
15
+
16
+ def initialize(relations, current_sobject, parent_association_field = nil)
17
+ @relations = [relations].flatten
18
+ @current_sobject = current_sobject
19
+ @association_mapping = {}
20
+ @parent_association_field = parent_association_field
21
+ @fields = []
22
+ end
23
+
24
+
25
+ def projections
26
+ relations.each do |relation|
27
+ case relation
28
+ when Symbol
29
+ association = current_sobject.associations[relation]
30
+ raise InvalidAssociationError, "Association named #{relation} was not found on #{current_sobject}" if association.nil?
31
+ build_includes(association)
32
+ when Hash
33
+ build_hash_includes(relation)
34
+ end
35
+ end
36
+ { fields: fields, association_mapping: association_mapping }
37
+ end
38
+
39
+ def build_includes(association)
40
+ fields.concat(EagerLoadProjectionBuilder.build(association, parent_association_field))
41
+ association_mapping[association.sfdc_association_field.downcase] = association.relation_name
42
+ end
43
+
44
+ def build_hash_includes(relation, model = current_sobject, parent_association_field = nil)
45
+ relation.each do |key, value|
46
+ association = model.associations[key]
47
+ raise InvalidAssociationError, "Association named #{key} was not found on #{model}" if association.nil?
48
+ case association
49
+ when ActiveForce::Association::BelongsToAssociation
50
+ build_includes(association)
51
+ nested_query = build_relation_for_belongs_to(association, value)
52
+ fields.concat(nested_query[:fields])
53
+ association_mapping.merge!(nested_query[:association_mapping])
54
+ else
55
+ nested_query = build_relation(association, value)
56
+ fields.concat(nested_query[:fields])
57
+ association_mapping.merge!(nested_query[:association_mapping])
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def build_relation(association, nested_includes)
65
+ sub_query = Query.new(association.sfdc_association_field)
66
+ sub_query.fields association.relation_model.fields
67
+ association_mapping[association.sfdc_association_field.downcase] = association.relation_name
68
+ nested_includes_query = self.class.build(nested_includes, association.relation_model)
69
+ sub_query.fields nested_includes_query[:fields]
70
+ { fields: ["(#{sub_query})"], association_mapping: nested_includes_query[:association_mapping] }
71
+ end
72
+
73
+
74
+ def build_relation_for_belongs_to(association, nested_includes)
75
+ if parent_association_field.present?
76
+ current_parent_association_field = "#{parent_association_field}.#{association.sfdc_association_field}"
77
+ else
78
+ current_parent_association_field = association.sfdc_association_field
79
+ end
80
+ self.class.build(nested_includes, association.relation_model, current_parent_association_field)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -2,31 +2,33 @@ module ActiveForce
2
2
  module Association
3
3
  class EagerLoadProjectionBuilder
4
4
  class << self
5
- def build(association)
6
- new(association).projections
5
+ def build(association, parent_association_field = nil)
6
+ new(association, parent_association_field).projections
7
7
  end
8
8
  end
9
9
 
10
- attr_reader :association
10
+ attr_reader :association, :parent_association_field
11
11
 
12
- def initialize(association)
12
+ def initialize(association, parent_association_field = nil)
13
13
  @association = association
14
+ @parent_association_field = parent_association_field
14
15
  end
15
16
 
16
17
  def projections
17
18
  klass = association.class.name.split('::').last
18
19
  builder_class = ActiveForce::Association.const_get "#{klass}ProjectionBuilder"
19
- builder_class.new(association).projections
20
+ builder_class.new(association, parent_association_field).projections
20
21
  rescue NameError
21
22
  raise "Don't know how to build projections for #{klass}"
22
23
  end
23
24
  end
24
25
 
25
26
  class AbstractProjectionBuilder
26
- attr_reader :association
27
+ attr_reader :association, :parent_association_field
27
28
 
28
- def initialize(association)
29
+ def initialize(association, parent_association_field = nil)
29
30
  @association = association
31
+ @parent_association_field = parent_association_field
30
32
  end
31
33
 
32
34
  def projections
@@ -57,8 +59,13 @@ module ActiveForce
57
59
 
58
60
  class BelongsToAssociationProjectionBuilder < AbstractProjectionBuilder
59
61
  def projections
62
+ association_field = if parent_association_field.present?
63
+ "#{ parent_association_field }.#{ association.sfdc_association_field }"
64
+ else
65
+ association.sfdc_association_field
66
+ end
60
67
  association.relation_model.fields.map do |field|
61
- "#{ association.sfdc_association_field }.#{ field }"
68
+ "#{ association_field }.#{ field }"
62
69
  end
63
70
  end
64
71
  end
@@ -2,19 +2,20 @@ module ActiveForce
2
2
  module Association
3
3
  class RelationModelBuilder
4
4
  class << self
5
- def build(association, value)
6
- new(association, value).build_relation_model
5
+ def build(association, value, association_mapping = {})
6
+ new(association, value, association_mapping).build_relation_model
7
7
  end
8
8
  end
9
9
 
10
- def initialize(association, value)
10
+ def initialize(association, value, association_mapping = {})
11
11
  @association = association
12
12
  @value = value
13
+ @association_mapping = association_mapping
13
14
  end
14
15
 
15
16
  def build_relation_model
16
17
  klass = resolve_class
17
- klass.new(@association, @value).call
18
+ klass.new(@association, @value, @association_mapping).call
18
19
  end
19
20
 
20
21
  private
@@ -28,11 +29,12 @@ module ActiveForce
28
29
  end
29
30
 
30
31
  class AbstractBuildFrom
31
- attr_reader :association, :value
32
+ attr_reader :association, :value, :association_mapping
32
33
 
33
- def initialize(association, value)
34
+ def initialize(association, value, association_mapping = {})
34
35
  @association = association
35
36
  @value = value
37
+ @association_mapping = association_mapping
36
38
  end
37
39
 
38
40
  def call
@@ -42,13 +44,13 @@ module ActiveForce
42
44
 
43
45
  class BuildFromHash < AbstractBuildFrom
44
46
  def call
45
- association.build value
47
+ association.build(value, association_mapping)
46
48
  end
47
49
  end
48
50
 
49
51
  class BuildFromArray < AbstractBuildFrom
50
52
  def call
51
- value.map { |mash| association.build mash }
53
+ value.map { |mash| association.build(mash, association_mapping) }
52
54
  end
53
55
  end
54
56
 
@@ -4,6 +4,7 @@ require 'active_force/association/relation_model_builder'
4
4
  require 'active_force/association/has_many_association'
5
5
  require 'active_force/association/has_one_association'
6
6
  require 'active_force/association/belongs_to_association'
7
+ require 'active_force/association/eager_load_builder_for_nested_includes'
7
8
 
8
9
  module ActiveForce
9
10
  module Association
@@ -68,7 +68,7 @@ module ActiveForce
68
68
  if association_mapping.has_key?(column.downcase)
69
69
  column = association_mapping[column.downcase]
70
70
  end
71
- sobject.write_value column, value
71
+ sobject.write_value column, value, association_mapping
72
72
  end
73
73
  end
74
74
  sobject.clear_changes_information
@@ -173,10 +173,10 @@ module ActiveForce
173
173
  self
174
174
  end
175
175
 
176
- def write_value key, value
176
+ def write_value key, value, association_mapping = {}
177
177
  if association = self.class.find_association(key.to_sym)
178
178
  field = association.relation_name
179
- value = Association::RelationModelBuilder.build(association, value)
179
+ value = Association::RelationModelBuilder.build(association, value, association_mapping)
180
180
  elsif key.to_sym.in?(mappings.keys)
181
181
  # key is a field name
182
182
  field = key
@@ -227,9 +227,13 @@ module ActiveForce
227
227
  end
228
228
 
229
229
  def attributes_for_create
230
- @attributes.each_value.select { |value| value.is_a?(ActiveModel::Attribute::UserProvidedDefault) }
231
- .map(&:name)
232
- .concat(changed)
230
+ default_attributes.concat(changed)
231
+ end
232
+
233
+ def default_attributes
234
+ @attributes.each_value.select do |value|
235
+ value.is_a?(ActiveModel::Attribute::UserProvidedDefault) || value.instance_values["original_attribute"].is_a?(ActiveModel::Attribute::UserProvidedDefault)
236
+ end.map(&:name)
233
237
  end
234
238
 
235
239
  def attributes_for_update
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveForce
4
- VERSION = '0.15.1'
4
+ VERSION = '0.16.0'
5
5
  end
@@ -376,7 +376,7 @@ describe ActiveForce::SObject do
376
376
  it 'allows passing a foreign key' do
377
377
  Comment.belongs_to :post, foreign_key: :fancy_post_id
378
378
  allow(comment).to receive(:fancy_post_id).and_return "2"
379
- expect(client).to receive(:query).with("SELECT Id, Title__c FROM Post__c WHERE (Id = '2') LIMIT 1")
379
+ expect(client).to receive(:query).with("SELECT Id, Title__c, BlogId FROM Post__c WHERE (Id = '2') LIMIT 1")
380
380
  comment.post
381
381
  Comment.belongs_to :post # reset association to original value
382
382
  end
@@ -285,6 +285,331 @@ module ActiveForce
285
285
  expect(account.owner.id).to eq '321'
286
286
  end
287
287
  end
288
+
289
+ context 'when invalid associations are passed' do
290
+ it 'raises an error' do
291
+ expect { Quota.includes(:invalid).find('123') }.to raise_error(ActiveForce::Association::InvalidAssociationError, 'Association named invalid was not found on Quota')
292
+ end
293
+ end
294
+ end
295
+
296
+ describe '.includes with nested associations' do
297
+
298
+ context 'with custom objects' do
299
+ it 'formulates the correct SOQL query' do
300
+ soql = Quota.includes(prez_clubs: :club_members).where(id: '123').to_s
301
+ expect(soql).to eq <<-SOQL.squish
302
+ SELECT Id, Bar_Id__c,
303
+ (SELECT Id, QuotaId,
304
+ (SELECT Id, Name, Email FROM ClubMembers__r)
305
+ FROM PrezClubs__r)
306
+ FROM Quota__c
307
+ WHERE (Bar_Id__c = '123')
308
+ SOQL
309
+ end
310
+
311
+ it 'builds the associated objects and caches them' do
312
+ response = [build_restforce_sobject({
313
+ 'Id' => '123',
314
+ 'PrezClubs__r' => build_restforce_collection([
315
+ {'Id' => '213', 'QuotaId' => '123', 'ClubMembers__r' => build_restforce_collection([
316
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com'},
317
+ {'Id' => '214', 'Name' => 'def', 'Email' => 'def@af.com'}
318
+ ])},
319
+ {'Id' => '214', 'QuotaId' => '123', 'ClubMembers__r' => build_restforce_collection([
320
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com'},
321
+ {'Id' => '214', 'Name' => 'def', 'Email' => 'def@af.com'}
322
+ ])}
323
+ ])
324
+ })]
325
+ allow(client).to receive(:query).once.and_return response
326
+ account = Quota.includes(prez_clubs: :club_members).find '123'
327
+ expect(account.prez_clubs).to be_an Array
328
+ expect(account.prez_clubs.all? { |o| o.is_a? PrezClub }).to eq true
329
+ expect(account.prez_clubs.first.club_members).to be_an Array
330
+ expect(account.prez_clubs.first.club_members.all? { |o| o.is_a? ClubMember }).to eq true
331
+ expect(account.prez_clubs.first.club_members.first.id).to eq '213'
332
+ expect(account.prez_clubs.first.club_members.first.name).to eq 'abc'
333
+ expect(account.prez_clubs.first.club_members.first.email).to eq 'abc@af.com'
334
+ end
335
+ end
336
+
337
+ context 'with namespaced sobjects' do
338
+ it 'formulates the correct SOQL query' do
339
+ soql = Salesforce::Account.includes({partner_opportunities: :owner}).where(id: '123').to_s
340
+ expect(soql).to eq <<-SOQL.squish
341
+ SELECT Id, Business_Partner__c,
342
+ (SELECT Id, OwnerId, AccountId, Business_Partner__c, Owner.Id
343
+ FROM Opportunities)
344
+ FROM Account
345
+ WHERE (Id = '123')
346
+ SOQL
347
+ end
348
+
349
+ it 'builds the associated objects and caches them' do
350
+ response = [build_restforce_sobject({
351
+ 'Id' => '123',
352
+ 'opportunities' => build_restforce_collection([
353
+ {'Id' => '213', 'AccountId' => '123', 'OwnerId' => '321', 'Business_Partner__c' => '123', 'Owner' => {'Id' => '321'}},
354
+ {'Id' => '214', 'AccountId' => '123', 'OwnerId' => '321', 'Business_Partner__c' => '123', 'Owner' => {'Id' => '321'}} ])
355
+ })]
356
+ allow(client).to receive(:query).once.and_return response
357
+ account = Salesforce::Account.includes({partner_opportunities: :owner}).find '123'
358
+ expect(account.partner_opportunities).to be_an Array
359
+ expect(account.partner_opportunities.all? { |o| o.is_a? Salesforce::Opportunity }).to eq true
360
+ expect(account.partner_opportunities.first.owner).to be_a Salesforce::User
361
+ expect(account.partner_opportunities.first.owner.id).to eq '321'
362
+ end
363
+ end
364
+
365
+ context 'an array association nested within a hash association' do
366
+ it 'formulates the correct SOQL query' do
367
+ soql = Club.includes(book_clubs: [:club_members, :books]).where(id: '123').to_s
368
+ expect(soql).to eq <<-SOQL.squish
369
+ SELECT Id,
370
+ (SELECT Id, Name, Location,
371
+ (SELECT Id, Name, Email FROM ClubMembers__r),
372
+ (SELECT Id, Title, Author FROM Books__r)
373
+ FROM BookClubs__r)
374
+ FROM Club__c
375
+ WHERE (Id = '123')
376
+ SOQL
377
+ end
378
+
379
+ it 'builds the associated objects and caches them' do
380
+ response = [build_restforce_sobject({
381
+ 'Id' => '123',
382
+ 'BookClubs__r' => build_restforce_collection([
383
+ {
384
+ 'Id' => '213',
385
+ 'Name' => 'abc',
386
+ 'Location' => 'abc_location',
387
+ 'ClubMembers__r' => build_restforce_collection([{'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com'},{'Id' => '214', 'Name' => 'def', 'Email' => 'def@af.com'}]),
388
+ 'Books__r' => build_restforce_collection([{'Id' => '213', 'Title' => 'Foo', 'Author' => 'author1'},{'Id' => '214', 'Title' => 'Bar', 'Author' => 'author2'}]),
389
+ },
390
+ {
391
+ 'Id' => '214',
392
+ 'Name' => 'def',
393
+ 'Location' => 'def_location',
394
+ 'ClubMembers__r' => build_restforce_collection([{'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com'},{'Id' => '214', 'Name' => 'def', 'Email' => 'def@af.com'}]),
395
+ 'Books__r' => build_restforce_collection([{'Id' => '213', 'Title' => 'Foo', 'Author' => 'author1'},{'Id' => '214', 'Title' => 'Bar', 'Author' => 'author2'}]),
396
+ }
397
+ ])
398
+ })]
399
+ allow(client).to receive(:query).once.and_return response
400
+ club = Club.includes(book_clubs: [:club_members, :books]).find(id: '123')
401
+ expect(club.book_clubs).to be_an Array
402
+ expect(club.book_clubs.all? { |o| o.is_a? BookClub }).to eq true
403
+ expect(club.book_clubs.first.name).to eq 'abc'
404
+ expect(club.book_clubs.first.location).to eq 'abc_location'
405
+ expect(club.book_clubs.first.club_members).to be_an Array
406
+ expect(club.book_clubs.first.club_members.all? { |o| o.is_a? ClubMember }).to eq true
407
+ expect(club.book_clubs.first.club_members.first.id).to eq '213'
408
+ expect(club.book_clubs.first.club_members.first.name).to eq 'abc'
409
+ expect(club.book_clubs.first.club_members.first.email).to eq 'abc@af.com'
410
+ expect(club.book_clubs.first.books).to be_an Array
411
+ expect(club.book_clubs.first.books.all? { |o| o.is_a? Book }).to eq true
412
+ expect(club.book_clubs.first.books.first.id).to eq '213'
413
+ expect(club.book_clubs.first.books.first.title).to eq 'Foo'
414
+ expect(club.book_clubs.first.books.first.author).to eq 'author1'
415
+ end
416
+ end
417
+
418
+ context 'a hash association nested within a hash association' do
419
+ it 'formulates the correct SOQL query' do
420
+ soql = Club.includes(book_clubs: {club_members: :membership}).where(id: '123').to_s
421
+ expect(soql).to eq <<-SOQL.squish
422
+ SELECT Id,
423
+ (SELECT Id, Name, Location,
424
+ (SELECT Id, Name, Email,
425
+ (SELECT Id, Type, Club_Member_Id__c FROM Membership__r)
426
+ FROM ClubMembers__r)
427
+ FROM BookClubs__r)
428
+ FROM Club__c
429
+ WHERE (Id = '123')
430
+ SOQL
431
+ end
432
+
433
+ it 'builds the associated objects and caches them' do
434
+ response = [build_restforce_sobject({
435
+ 'Id' => '123',
436
+ 'BookClubs__r' => build_restforce_collection([
437
+ {
438
+ 'Id' => '213',
439
+ 'Name' => 'abc',
440
+ 'Location' => 'abc_location',
441
+ 'ClubMembers__r' => build_restforce_collection([
442
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '111', 'Type' => 'Gold'}])},
443
+ {'Id' => '214', 'Name' => 'abc', 'Email' => 'abc@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '222', 'Type' => 'Silver'}])},
444
+ ]),
445
+ },
446
+ {
447
+ 'Id' => '214',
448
+ 'Name' => 'def',
449
+ 'Location' => 'def_location',
450
+ 'ClubMembers__r' => build_restforce_collection([
451
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '111', 'Type' => 'Gold'}])},
452
+ {'Id' => '214', 'Name' => 'abc', 'Email' => 'abc@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '222', 'Type' => 'Silver'}])},
453
+ ]),
454
+ }
455
+ ])
456
+ })]
457
+ allow(client).to receive(:query).once.and_return response
458
+ club = Club.includes(book_clubs: {club_members: :membership}).find(id: '123')
459
+ expect(club.book_clubs).to be_an Array
460
+ expect(club.book_clubs.all? { |o| o.is_a? BookClub }).to eq true
461
+ expect(club.book_clubs.first.id).to eq '213'
462
+ expect(club.book_clubs.first.name).to eq 'abc'
463
+ expect(club.book_clubs.first.location).to eq 'abc_location'
464
+ expect(club.book_clubs.first.club_members).to be_an Array
465
+ expect(club.book_clubs.first.club_members.all? { |o| o.is_a? ClubMember }).to eq true
466
+ expect(club.book_clubs.first.club_members.first.id).to eq '213'
467
+ expect(club.book_clubs.first.club_members.first.name).to eq 'abc'
468
+ expect(club.book_clubs.first.club_members.first.email).to eq 'abc@af.com'
469
+ expect(club.book_clubs.first.club_members.first.membership).to be_a Membership
470
+ expect(club.book_clubs.first.club_members.first.membership.id).to eq '111'
471
+ expect(club.book_clubs.first.club_members.first.membership.type).to eq 'Gold'
472
+ end
473
+ end
474
+
475
+ context 'mulitple nested associations' do
476
+ it 'formulates the correct SOQL query' do
477
+ soql = Club.includes({prez_clubs: {club_members: :membership}}, {book_clubs: [:club_members, :books]}).where(id: '123').to_s
478
+ expect(soql).to eq <<-SOQL.squish
479
+ SELECT Id,
480
+ (SELECT Id, QuotaId,
481
+ (SELECT Id, Name, Email,
482
+ (SELECT Id, Type, Club_Member_Id__c FROM Membership__r)
483
+ FROM ClubMembers__r)
484
+ FROM PrezClubs__r),
485
+ (SELECT Id, Name, Location,
486
+ (SELECT Id, Name, Email FROM ClubMembers__r),
487
+ (SELECT Id, Title, Author FROM Books__r)
488
+ FROM BookClubs__r)
489
+ FROM Club__c
490
+ WHERE (Id = '123')
491
+ SOQL
492
+ end
493
+
494
+ it 'builds the associated objects and caches them' do
495
+ response = [build_restforce_sobject({
496
+ 'Id' => '123',
497
+ 'PrezClubs__r' => build_restforce_collection([
498
+ {'Id' => '213', 'QuotaId' => '123', 'ClubMembers__r' => build_restforce_collection([
499
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '111', 'Type' => 'Gold'}])},
500
+ {'Id' => '214', 'Name' => 'def', 'Email' => 'def@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '222', 'Type' => 'Silver'}])},
501
+ ])},
502
+ {'Id' => '214', 'QuotaId' => '123', 'ClubMembers__r' => build_restforce_collection([
503
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '111', 'Type' => 'Gold'}])},
504
+ {'Id' => '214', 'Name' => 'def', 'Email' => 'def@af.com', 'Membership__r' => build_restforce_collection([{'Id' => '222', 'Type' => 'Silver'}])},
505
+ ])}
506
+ ]),
507
+ 'BookClubs__r' => build_restforce_collection([
508
+ {
509
+ 'Id' => '213',
510
+ 'Name' => 'abc',
511
+ 'Location' => 'abc_location',
512
+ 'ClubMembers__r' => build_restforce_collection([
513
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com'},
514
+ {'Id' => '214', 'Name' => 'abc', 'Email' => 'abc@af.com'},
515
+ ]),
516
+ 'Books__r' => build_restforce_collection([
517
+ {'Id' => '213', 'Title' => 'Foo', 'Author' => 'author1'},
518
+ {'Id' => '214', 'Title' => 'Bar', 'Author' => 'author2'},
519
+ ])
520
+ },
521
+ {
522
+ 'Id' => '214',
523
+ 'Name' => 'def',
524
+ 'Location' => 'def_location',
525
+ 'ClubMembers__r' => build_restforce_collection([
526
+ {'Id' => '213', 'Name' => 'abc', 'Email' => 'abc@af.com'},
527
+ {'Id' => '214', 'Name' => 'abc', 'Email' => 'abc@af.com'},
528
+ ]),
529
+ 'Books__r' => build_restforce_collection([
530
+ {'Id' => '213', 'Title' => 'Foo', 'Author' => 'author1'},
531
+ {'Id' => '214', 'Title' => 'Bar', 'Author' => 'author2'},
532
+ ])
533
+ }
534
+ ])
535
+ })]
536
+ allow(client).to receive(:query).once.and_return response
537
+ club = Club.includes({prez_clubs: {club_members: :membership}}, {book_clubs: [:club_members, :books]}).find(id: '123')
538
+ expect(club.prez_clubs).to be_an Array
539
+ expect(club.prez_clubs.all? { |o| o.is_a? PrezClub }).to eq true
540
+ expect(club.prez_clubs.first.club_members).to be_an Array
541
+ expect(club.prez_clubs.first.club_members.all? { |o| o.is_a? ClubMember }).to eq true
542
+ expect(club.prez_clubs.first.club_members.first.id).to eq '213'
543
+ expect(club.prez_clubs.first.club_members.first.name).to eq 'abc'
544
+ expect(club.prez_clubs.first.club_members.first.email).to eq 'abc@af.com'
545
+ expect(club.prez_clubs.first.club_members.first.membership).to be_a Membership
546
+ expect(club.prez_clubs.first.club_members.first.membership.id).to eq '111'
547
+ expect(club.prez_clubs.first.club_members.first.membership.type).to eq 'Gold'
548
+ expect(club.book_clubs).to be_an Array
549
+ expect(club.book_clubs.all? { |o| o.is_a? BookClub }).to eq true
550
+ expect(club.book_clubs.first.id).to eq '213'
551
+ expect(club.book_clubs.first.name).to eq 'abc'
552
+ expect(club.book_clubs.first.location).to eq 'abc_location'
553
+ expect(club.book_clubs.first.club_members).to be_an Array
554
+ expect(club.book_clubs.first.club_members.all? { |o| o.is_a? ClubMember }).to eq true
555
+ expect(club.book_clubs.first.club_members.first.id).to eq '213'
556
+ expect(club.book_clubs.first.club_members.first.name).to eq 'abc'
557
+ expect(club.book_clubs.first.club_members.first.email).to eq 'abc@af.com'
558
+ expect(club.book_clubs.first.books).to be_an Array
559
+ expect(club.book_clubs.first.books.all? { |o| o.is_a? Book }).to eq true
560
+ expect(club.book_clubs.first.books.first.id).to eq '213'
561
+ expect(club.book_clubs.first.books.first.title).to eq 'Foo'
562
+ expect(club.book_clubs.first.books.first.author).to eq 'author1'
563
+ end
564
+ end
565
+
566
+ context 'nested belongs-to association' do
567
+ it 'formulates the correct SOQL query' do
568
+ soql = Comment.includes(post: :blog).where(id: '123').to_s
569
+ expect(soql).to eq <<-SOQL.squish
570
+ SELECT Id, PostId, PosterId__c, FancyPostId, Body__c,
571
+ PostId.Id, PostId.Title__c, PostId.BlogId,
572
+ PostId.BlogId.Id, PostId.BlogId.Name, PostId.BlogId.Link__c
573
+ FROM Comment__c
574
+ WHERE (Id = '123')
575
+ SOQL
576
+ end
577
+
578
+ it 'builds the associated objects and caches them' do
579
+ response = [build_restforce_sobject({
580
+ 'Id' => '123',
581
+ 'PostId' => '321',
582
+ 'PosterId__c' => '432',
583
+ 'FancyPostId' => '543',
584
+ 'Body__c' => 'body',
585
+ 'PostId' => {
586
+ 'Id' => '321',
587
+ 'Title__c' => 'title',
588
+ 'BlogId' => '432',
589
+ 'BlogId' => {
590
+ 'Id' => '432',
591
+ 'Name' => 'name',
592
+ 'Link__c' => 'link'
593
+ }
594
+ }
595
+ })]
596
+ allow(client).to receive(:query).once.and_return response
597
+ comment = Comment.includes(post: :blog).find '123'
598
+ expect(comment.post).to be_a Post
599
+ expect(comment.post.id).to eq '321'
600
+ expect(comment.post.title).to eq 'title'
601
+ expect(comment.post.blog).to be_a Blog
602
+ expect(comment.post.blog.id).to eq '432'
603
+ expect(comment.post.blog.name).to eq 'name'
604
+ expect(comment.post.blog.link).to eq 'link'
605
+ end
606
+ end
607
+
608
+ context 'when invalid nested associations are passed' do
609
+ it 'raises an error' do
610
+ expect { Quota.includes(prez_clubs: :invalid).find('123') }.to raise_error(ActiveForce::Association::InvalidAssociationError, 'Association named invalid was not found on PrezClub')
611
+ end
612
+ end
288
613
  end
289
614
  end
290
615
  end
@@ -96,6 +96,20 @@ describe ActiveForce::SObject do
96
96
  it 'sets percent field upon object instantiation' do
97
97
  expect(subject.new(**instantiation_attributes)[:percent]).to eq(percent)
98
98
  end
99
+
100
+ context 'when the override to default value is the same as the default value' do
101
+ let(:percent) { 50.0 }
102
+
103
+ it 'sends percent to salesforce' do
104
+ expect(client).to receive(:create!)
105
+ .with(anything, hash_including('Percent_Label' => percent))
106
+ subject.create(**instantiation_attributes)
107
+ end
108
+
109
+ it 'sets percent field upon object instantiation' do
110
+ expect(subject.new(**instantiation_attributes)[:percent]).to eq(percent)
111
+ end
112
+ end
99
113
  end
100
114
  end
101
115
 
@@ -10,6 +10,7 @@ end
10
10
  class Post < ActiveForce::SObject
11
11
  self.table_name = "Post__c"
12
12
  field :title
13
+ field :blog_id, from: "BlogId"
13
14
  has_many :comments
14
15
  has_many :impossible_comments, model: Comment, scoped_as: ->{ where('1 = 0') }
15
16
  has_many :reply_comments, model: Comment, scoped_as: ->(post){ where(body: "RE: #{post.title}").order('CreationDate DESC') }
@@ -17,6 +18,13 @@ class Post < ActiveForce::SObject
17
18
  has_many :poster_comments, { foreign_key: :poster_id, model: Comment }
18
19
  has_one :last_comment, model: Comment, scoped_as: -> { where.not(body: nil).order('CreatedDate DESC') }
19
20
  has_one :repeat_comment, model: Comment, scoped_as: ->(post) { where(body: post.title) }
21
+ belongs_to :blog
22
+ end
23
+
24
+ class Blog < ActiveForce::SObject
25
+ field :name, from: 'Name'
26
+ field :link, from: 'Link__c'
27
+ has_many :posts
20
28
  end
21
29
  class Territory < ActiveForce::SObject
22
30
  field :quota_id, from: "Quota__c"
@@ -26,7 +34,40 @@ end
26
34
  class PrezClub < ActiveForce::SObject
27
35
  field :quota_id, from: 'QuotaId'
28
36
  belongs_to :quota
37
+ has_many :club_members
38
+ has_one :club_owner
39
+ end
40
+
41
+ class Club < ActiveForce::SObject
42
+ has_many :book_clubs
43
+ has_many :prez_clubs
44
+ end
45
+
46
+ class BookClub < ActiveForce::SObject
47
+ field :name, from: 'Name'
48
+ field :location, from: 'Location'
49
+ has_many :club_members
50
+ has_many :books
29
51
  end
52
+ class ClubMember < ActiveForce::SObject
53
+ field :name, from: 'Name'
54
+ field :email, from: 'Email'
55
+ has_one :membership
56
+ end
57
+
58
+ class Book < ActiveForce::SObject
59
+ field :title, from: 'Title'
60
+ field :author, from: 'Author'
61
+ end
62
+ class Membership < ActiveForce::SObject
63
+ field :type, from: 'Type'
64
+ field :club_member_id, from: 'Club_Member_Id__c'
65
+ end
66
+
67
+ class ClubOwner < ActiveForce::SObject
68
+ field :name, from: 'Name'
69
+ end
70
+
30
71
  class Quota < ActiveForce::SObject
31
72
  field :id, from: 'Bar_Id__c'
32
73
  has_many :prez_clubs
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_force
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eloy Espinaco
8
8
  - Pablo Oldani
9
9
  - Armando Andini
10
10
  - José Piccioni
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2023-05-31 00:00:00.000000000 Z
14
+ date: 2023-07-31 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activemodel
@@ -150,6 +150,7 @@ files:
150
150
  - lib/active_force/association.rb
151
151
  - lib/active_force/association/association.rb
152
152
  - lib/active_force/association/belongs_to_association.rb
153
+ - lib/active_force/association/eager_load_builder_for_nested_includes.rb
153
154
  - lib/active_force/association/eager_load_projection_builder.rb
154
155
  - lib/active_force/association/has_many_association.rb
155
156
  - lib/active_force/association/has_one_association.rb
@@ -192,7 +193,7 @@ metadata:
192
193
  bug_tracker_uri: https://github.com/Beyond-Finance/active_force/issues
193
194
  changelog_uri: https://github.com/Beyond-Finance/active_force/blob/main/CHANGELOG.md
194
195
  source_code_uri: https://github.com/Beyond-Finance/active_force
195
- post_install_message:
196
+ post_install_message:
196
197
  rdoc_options: []
197
198
  require_paths:
198
199
  - lib
@@ -207,8 +208,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
208
  - !ruby/object:Gem::Version
208
209
  version: '0'
209
210
  requirements: []
210
- rubygems_version: 3.4.6
211
- signing_key:
211
+ rubygems_version: 3.1.6
212
+ signing_key:
212
213
  specification_version: 4
213
214
  summary: Help you implement models persisting on Sales Force within Rails using RESTForce
214
215
  test_files: