active_force 0.23.0 → 0.24.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 +3 -0
- data/README.md +11 -0
- data/lib/active_force/active_query.rb +9 -4
- data/lib/active_force/association/eager_load_builder_for_nested_includes.rb +16 -8
- data/lib/active_force/association/eager_load_projection_builder.rb +13 -11
- data/lib/active_force/query.rb +1 -1
- data/lib/active_force/select_builder.rb +41 -0
- data/lib/active_force/version.rb +1 -1
- data/spec/active_force/sobject/includes_spec.rb +40 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e6bf4ca3441ea8c1d0df00f6a8925f021fa53a3a0ce2e19beda69ecab4b5881
|
4
|
+
data.tar.gz: 940b3627880eb3a67d8387c744246a8bb29cc816ccf828130dd375e1e33b1791
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7ab1dc49814c642b34b61391a2d8ab99fc2b0854f3113b5c4d5715a67ec681d3061fa2e3e53eea3ec9780525aeba3804c7043901b7652baa67113d2659bd8dc
|
7
|
+
data.tar.gz: d69e76e37be28552a80a9a3e2872a48ff15e04df4178f5ce2b670426be23570a7ca11256e41ed757ba04fc20270bff875730d8aea2927eee7e0129124c5f994e
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
## Not released
|
4
4
|
|
5
|
+
## 0.24.0
|
6
|
+
- Add support for nested select statements that are used in conjuction with nested includes (https://github.com/Beyond-Finance/active_force/pull/102)
|
7
|
+
|
5
8
|
## 0.23.0
|
6
9
|
- Partially addresses #90. `#select` accepts a block and returns an array of filtered SObjects. (https://github.com/Beyond-Finance/active_force/pull/99)
|
7
10
|
|
data/README.md
CHANGED
@@ -218,6 +218,17 @@ Comment.includes(post: :owner)
|
|
218
218
|
Comment.includes({post: {owner: :account}})
|
219
219
|
```
|
220
220
|
|
221
|
+
You can also use #select with a multi level #includes.
|
222
|
+
|
223
|
+
Examples:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
Comment.select(:body, post: [:title, :is_active]).includes(post: :owner)
|
227
|
+
Comment.select(:body, account: :owner_id).includes({post: {owner: :account}})
|
228
|
+
```
|
229
|
+
|
230
|
+
The Sobject name in the #select must match the Sobject in the #includes for the fields to be filtered.
|
231
|
+
|
221
232
|
### Aggregates
|
222
233
|
|
223
234
|
Summing the values of a column:
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'active_support/all'
|
2
2
|
require 'active_force/query'
|
3
|
+
require 'active_force/select_builder'
|
3
4
|
require 'forwardable'
|
4
5
|
|
5
6
|
module ActiveForce
|
@@ -25,7 +26,7 @@ module ActiveForce
|
|
25
26
|
class ActiveQuery < Query
|
26
27
|
extend Forwardable
|
27
28
|
|
28
|
-
attr_reader :sobject, :association_mapping, :belongs_to_association_mapping
|
29
|
+
attr_reader :sobject, :association_mapping, :belongs_to_association_mapping, :nested_query_fields
|
29
30
|
|
30
31
|
def_delegators :sobject, :sfdc_client, :build, :table_name, :mappings
|
31
32
|
def_delegators :to_a, :blank?, :present?, :any?, :each, :map, :inspect, :pluck, :each_with_object
|
@@ -36,6 +37,7 @@ module ActiveForce
|
|
36
37
|
@belongs_to_association_mapping = {}
|
37
38
|
super custom_table_name || table_name
|
38
39
|
fields sobject.fields
|
40
|
+
@nested_query_fields = []
|
39
41
|
end
|
40
42
|
|
41
43
|
def to_a
|
@@ -86,8 +88,11 @@ module ActiveForce
|
|
86
88
|
end
|
87
89
|
result
|
88
90
|
else
|
89
|
-
|
90
|
-
|
91
|
+
fields_collection = ActiveForce::SelectBuilder.new(selected_fields, self).parse
|
92
|
+
nested_query_fields.concat(fields_collection[:nested_query_fields]) if fields_collection[:nested_query_fields]
|
93
|
+
return self if fields_collection[:non_nested_query_fields].blank?
|
94
|
+
|
95
|
+
super *fields_collection[:non_nested_query_fields]
|
91
96
|
end
|
92
97
|
end
|
93
98
|
|
@@ -114,7 +119,7 @@ module ActiveForce
|
|
114
119
|
end
|
115
120
|
|
116
121
|
def includes(*relations)
|
117
|
-
includes_query = Association::EagerLoadBuilderForNestedIncludes.build(relations, sobject)
|
122
|
+
includes_query = Association::EagerLoadBuilderForNestedIncludes.build(relations, sobject, nil, nested_query_fields)
|
118
123
|
fields includes_query[:fields]
|
119
124
|
association_mapping.merge!(includes_query[:association_mapping])
|
120
125
|
self
|
@@ -6,18 +6,19 @@ module ActiveForce
|
|
6
6
|
class EagerLoadBuilderForNestedIncludes
|
7
7
|
|
8
8
|
class << self
|
9
|
-
def build(relations, current_sobject, parent_association_field = nil)
|
10
|
-
new(relations, current_sobject, parent_association_field).projections
|
9
|
+
def build(relations, current_sobject, parent_association_field = nil, query_fields = nil)
|
10
|
+
new(relations, current_sobject, parent_association_field, query_fields).projections
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
attr_reader :relations, :current_sobject, :association_mapping, :parent_association_field, :fields
|
14
|
+
attr_reader :relations, :current_sobject, :association_mapping, :parent_association_field, :fields, :query_fields
|
15
15
|
|
16
|
-
def initialize(relations, current_sobject, parent_association_field = nil)
|
16
|
+
def initialize(relations, current_sobject, parent_association_field = nil, query_fields = nil)
|
17
17
|
@relations = [relations].flatten
|
18
18
|
@current_sobject = current_sobject
|
19
19
|
@association_mapping = {}
|
20
20
|
@parent_association_field = parent_association_field
|
21
|
+
@query_fields = query_fields
|
21
22
|
@fields = []
|
22
23
|
end
|
23
24
|
|
@@ -37,10 +38,17 @@ module ActiveForce
|
|
37
38
|
end
|
38
39
|
|
39
40
|
def build_includes(association)
|
40
|
-
fields.concat(EagerLoadProjectionBuilder.build(association, parent_association_field))
|
41
|
+
fields.concat(EagerLoadProjectionBuilder.build(association, parent_association_field, query_fields_for(association)))
|
41
42
|
association_mapping[association.sfdc_association_field.downcase] = association.relation_name
|
42
43
|
end
|
43
44
|
|
45
|
+
def query_fields_for(association)
|
46
|
+
return nil if query_fields.blank?
|
47
|
+
query_fields_with_association = query_fields.find { |nested_field| nested_field[association.relation_name].present? }
|
48
|
+
return nil if query_fields_with_association.blank?
|
49
|
+
query_fields_with_association[association.relation_name].map { |field| association.relation_model.mappings[field] }
|
50
|
+
end
|
51
|
+
|
44
52
|
def build_hash_includes(relation, model = current_sobject, parent_association_field = nil)
|
45
53
|
relation.each do |key, value|
|
46
54
|
association = model.associations[key]
|
@@ -63,10 +71,10 @@ module ActiveForce
|
|
63
71
|
|
64
72
|
def build_relation(association, nested_includes)
|
65
73
|
builder_class = ActiveForce::Association::EagerLoadProjectionBuilder.projection_builder_class(association)
|
66
|
-
projection_builder = builder_class.new(association)
|
74
|
+
projection_builder = builder_class.new(association, nil, query_fields_for(association))
|
67
75
|
sub_query = projection_builder.query_with_association_fields
|
68
76
|
association_mapping[association.sfdc_association_field.downcase] = association.relation_name
|
69
|
-
nested_includes_query = self.class.build(nested_includes, association.relation_model)
|
77
|
+
nested_includes_query = self.class.build(nested_includes, association.relation_model, nil, query_fields)
|
70
78
|
sub_query.fields nested_includes_query[:fields]
|
71
79
|
{ fields: ["(#{sub_query})"], association_mapping: nested_includes_query[:association_mapping] }
|
72
80
|
end
|
@@ -78,7 +86,7 @@ module ActiveForce
|
|
78
86
|
else
|
79
87
|
current_parent_association_field = association.sfdc_association_field
|
80
88
|
end
|
81
|
-
self.class.build(nested_includes, association.relation_model, current_parent_association_field)
|
89
|
+
self.class.build(nested_includes, association.relation_model, current_parent_association_field, query_fields)
|
82
90
|
end
|
83
91
|
end
|
84
92
|
end
|
@@ -3,8 +3,8 @@ module ActiveForce
|
|
3
3
|
class InvalidEagerLoadAssociation < StandardError; end
|
4
4
|
class EagerLoadProjectionBuilder
|
5
5
|
class << self
|
6
|
-
def build(association, parent_association_field = nil)
|
7
|
-
new(association, parent_association_field).projections
|
6
|
+
def build(association, parent_association_field = nil, query_fields = nil)
|
7
|
+
new(association, parent_association_field, query_fields).projections
|
8
8
|
end
|
9
9
|
|
10
10
|
def projection_builder_class(association)
|
@@ -15,26 +15,27 @@ module ActiveForce
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
attr_reader :association, :parent_association_field
|
18
|
+
attr_reader :association, :parent_association_field, :query_fields
|
19
19
|
|
20
|
-
def initialize(association, parent_association_field = nil)
|
20
|
+
def initialize(association, parent_association_field = nil, query_fields = nil)
|
21
21
|
@association = association
|
22
22
|
@parent_association_field = parent_association_field
|
23
|
+
@query_fields = query_fields
|
23
24
|
end
|
24
25
|
|
25
26
|
def projections
|
26
27
|
builder_class = self.class.projection_builder_class(association)
|
27
|
-
builder_class.new(association, parent_association_field).projections
|
28
|
+
builder_class.new(association, parent_association_field, query_fields).projections
|
28
29
|
end
|
29
|
-
|
30
30
|
end
|
31
31
|
|
32
32
|
class AbstractProjectionBuilder
|
33
|
-
attr_reader :association, :parent_association_field
|
33
|
+
attr_reader :association, :parent_association_field, :query_fields
|
34
34
|
|
35
|
-
def initialize(association, parent_association_field = nil)
|
35
|
+
def initialize(association, parent_association_field = nil, query_fields = nil)
|
36
36
|
@association = association
|
37
37
|
@parent_association_field = parent_association_field
|
38
|
+
@query_fields = query_fields
|
38
39
|
end
|
39
40
|
|
40
41
|
def projections
|
@@ -54,8 +55,8 @@ module ActiveForce
|
|
54
55
|
# to be pluralized
|
55
56
|
def query_with_association_fields
|
56
57
|
relationship_name = association.sfdc_association_field
|
57
|
-
|
58
|
-
query.
|
58
|
+
selected_fields = query_fields || association.relation_model.fields
|
59
|
+
query = ActiveQuery.new(association.relation_model, relationship_name).select(*selected_fields)
|
59
60
|
apply_association_scope(query)
|
60
61
|
end
|
61
62
|
end
|
@@ -79,7 +80,8 @@ module ActiveForce
|
|
79
80
|
else
|
80
81
|
association.sfdc_association_field
|
81
82
|
end
|
82
|
-
association.relation_model.fields
|
83
|
+
selected_fields = query_fields || association.relation_model.fields
|
84
|
+
selected_fields.map do |field|
|
83
85
|
"#{ association_field }.#{ field }"
|
84
86
|
end
|
85
87
|
end
|
data/lib/active_force/query.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActiveForce
|
2
|
+
class SelectBuilder
|
3
|
+
|
4
|
+
attr_reader :selected_fields, :nested_query_fields, :non_nested_query_fields, :query
|
5
|
+
|
6
|
+
def initialize(selected_fields, query)
|
7
|
+
@query = query
|
8
|
+
@selected_fields = selected_fields
|
9
|
+
@non_nested_query_fields = []
|
10
|
+
@nested_query_fields = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse
|
14
|
+
selected_fields.each do |field|
|
15
|
+
case field
|
16
|
+
when Symbol
|
17
|
+
non_nested_query_fields << query.mappings[field]
|
18
|
+
when Hash
|
19
|
+
populate_nested_query_fields(field)
|
20
|
+
when String
|
21
|
+
non_nested_query_fields << field
|
22
|
+
end
|
23
|
+
end
|
24
|
+
{non_nested_query_fields: non_nested_query_fields, nested_query_fields: nested_query_fields}
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def populate_nested_query_fields(field)
|
30
|
+
field.each do |key, value|
|
31
|
+
case value
|
32
|
+
when Symbol
|
33
|
+
field[key] = [value]
|
34
|
+
when Hash
|
35
|
+
raise ArgumentError, 'Nested Hash is not supported in select statement, you may wish to use an Array'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
nested_query_fields << field
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/active_force/version.rb
CHANGED
@@ -43,6 +43,25 @@ module ActiveForce
|
|
43
43
|
expect(territory.quota.id).to eq "321"
|
44
44
|
end
|
45
45
|
|
46
|
+
context 'when nested select statement' do
|
47
|
+
it 'formulates the correct SOQL query' do
|
48
|
+
soql = Salesforce::Territory.select(:id, :quota_id, quota: :id).includes(:quota).where(id: '123').to_s
|
49
|
+
expect(soql).to eq "SELECT Id, QuotaId, QuotaId.Id FROM Territory WHERE (Id = '123')"
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'errors when correct format is not followed' do
|
53
|
+
expect{Salesforce::Territory.select(:id, :quota_id, quota: {id: :quote}).includes(:quota).where(id: '123').to_s}.to raise_error ArgumentError
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when nested includes statement' do
|
57
|
+
it 'formulates the correct SOQL query' do
|
58
|
+
soql = Comment.select(:post_id, :body, post: [:title, :is_active], blog: :name).includes(post: :blog).to_s
|
59
|
+
|
60
|
+
expect(soql).to eq "SELECT PostId, Body__c, PostId.Title__c, PostId.IsActive, PostId.BlogId.Name FROM Comment__c"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
46
65
|
context 'with namespaced SObjects' do
|
47
66
|
it 'queries the API for the associated record' do
|
48
67
|
soql = Salesforce::Territory.includes(:quota).where(id: '123').to_s
|
@@ -156,6 +175,20 @@ module ActiveForce
|
|
156
175
|
end
|
157
176
|
|
158
177
|
context 'has_many' do
|
178
|
+
context 'when nested select statement' do
|
179
|
+
it 'formulates the correct SOQL query' do
|
180
|
+
soql = Account.select(opportunities: :id).includes(:opportunities).where(id: '123').to_s
|
181
|
+
expect(soql).to eq "SELECT Id, OwnerId, (SELECT Id FROM Opportunities) FROM Account WHERE (Id = '123')"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'when normal select with nested includes' do
|
186
|
+
it 'formulates the correct SOQL query' do
|
187
|
+
soql = Blog.select(:id, :link).includes(posts: :comments).to_s
|
188
|
+
expect(soql).to eq "SELECT Id, Link__c, (SELECT Id, Title__c, BlogId, IsActive, (SELECT Id, PostId, PosterId__c, FancyPostId, Body__c FROM Comments__r) FROM Posts__r) FROM Blog__c"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
159
192
|
context 'with standard objects' do
|
160
193
|
it 'formulates the correct SOQL query' do
|
161
194
|
soql = Account.includes(:opportunities).where(id: '123').to_s
|
@@ -294,6 +327,13 @@ module ActiveForce
|
|
294
327
|
end
|
295
328
|
|
296
329
|
context 'has_one' do
|
330
|
+
context 'when nested select statement is present' do
|
331
|
+
it 'formulates the correct SOQL query' do
|
332
|
+
soql = ClubMember.select(:name, :email, membership: :type).includes(:membership).to_s
|
333
|
+
expect(soql).to eq "SELECT Name, Email, (SELECT Type FROM Membership__r) FROM ClubMember__c"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
297
337
|
context 'when assocation has a scope' do
|
298
338
|
it 'formulates the correct SOQL query with the scope applied' do
|
299
339
|
soql = Post.includes(:last_comment).where(id: '1234').to_s
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_force
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.24.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eloy Espinaco
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2024-
|
14
|
+
date: 2024-10-01 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activemodel
|
@@ -163,6 +163,7 @@ files:
|
|
163
163
|
- lib/active_force/field.rb
|
164
164
|
- lib/active_force/mapping.rb
|
165
165
|
- lib/active_force/query.rb
|
166
|
+
- lib/active_force/select_builder.rb
|
166
167
|
- lib/active_force/sobject.rb
|
167
168
|
- lib/active_force/standard_types.rb
|
168
169
|
- lib/active_force/table.rb
|
@@ -219,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
219
220
|
- !ruby/object:Gem::Version
|
220
221
|
version: '0'
|
221
222
|
requirements: []
|
222
|
-
rubygems_version: 3.
|
223
|
+
rubygems_version: 3.4.10
|
223
224
|
signing_key:
|
224
225
|
specification_version: 4
|
225
226
|
summary: Help you implement models persisting on Sales Force within Rails using RESTForce
|