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