active_force 0.15.1 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
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: