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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +15 -0
- data/lib/active_force/active_query.rb +7 -10
- data/lib/active_force/association/belongs_to_association.rb +5 -1
- data/lib/active_force/association/eager_load_builder_for_nested_includes.rb +84 -0
- data/lib/active_force/association/eager_load_projection_builder.rb +15 -8
- data/lib/active_force/association/relation_model_builder.rb +10 -8
- data/lib/active_force/association.rb +1 -0
- data/lib/active_force/sobject.rb +10 -6
- data/lib/active_force/version.rb +1 -1
- data/spec/active_force/association_spec.rb +1 -1
- data/spec/active_force/sobject/includes_spec.rb +325 -0
- data/spec/active_force/sobject_spec.rb +14 -0
- data/spec/support/sobjects.rb +41 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7691f9f6f82d893a6aafdf29cfc6228ab2fe31988fafcf5be95070347a0286de
|
4
|
+
data.tar.gz: 4d0da25e5e12f9014407dba1871953a0b05f58227188d53130eba27dd537aa4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
97
|
-
|
98
|
-
|
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.
|
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
|
-
"#{
|
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
|
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
|
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
|
data/lib/active_force/sobject.rb
CHANGED
@@ -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
|
-
|
231
|
-
|
232
|
-
|
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
|
data/lib/active_force/version.rb
CHANGED
@@ -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
|
|
data/spec/support/sobjects.rb
CHANGED
@@ -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.
|
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-
|
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.
|
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:
|