active_force 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36b5f3173ab1e4adb37ab96a731723843304a96b658c41c946c65fb9d5d44b30
4
- data.tar.gz: 9b2a90628f126b3df5232480282e931a3518a85d8d7a321fefd53e7eccb91e74
3
+ metadata.gz: 0e6bf4ca3441ea8c1d0df00f6a8925f021fa53a3a0ce2e19beda69ecab4b5881
4
+ data.tar.gz: 940b3627880eb3a67d8387c744246a8bb29cc816ccf828130dd375e1e33b1791
5
5
  SHA512:
6
- metadata.gz: bf3392ac7ee848d2b4634002b9d6f7ac3983efff47aa30ecbaf4328c386a258220695251399d3974c1d53f3145b1e73dd31b9d29bad874730f7f67a346cea804
7
- data.tar.gz: fa747a0140dd738b13e8956e58fac620af22b37fb896aa503273e8dec73a7c00bb7f1ea8cb87325286ea6c1afa52d139211e083178b5a406863901936ba31a99
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
- selected_fields.map! { |field| mappings[field] }
90
- super *selected_fields
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
- query = ActiveQuery.new(association.relation_model, relationship_name)
58
- query.fields association.relation_model.fields
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.map do |field|
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
@@ -1,6 +1,6 @@
1
1
  module ActiveForce
2
2
  class Query
3
- attr_reader :table
3
+ attr_reader :table, :query_fields
4
4
 
5
5
  def initialize table
6
6
  @table = table
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveForce
4
- VERSION = '0.23.0'
4
+ VERSION = '0.24.0'
5
5
  end
@@ -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.23.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-09-27 00:00:00.000000000 Z
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.5.6
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