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 +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:
|