active_force 0.15.1 → 0.17.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: c325fe186d67cce9500a1df17360184bd97c2fa26fed2819b327f3ea8331df7c
4
- data.tar.gz: 482d5c2fabed72c3d12b646037dad7900f26e17260000a65e171d42ab41ce394
3
+ metadata.gz: 741f33a6c27d6bc11eaaa7655c2a7fb2ecc772f93d298e608866840b82e94321
4
+ data.tar.gz: 465abe2c572e35dc61817ab1bf8ac1d6d0e35e6f26eb2e933af3ed1781c2c68a
5
5
  SHA512:
6
- metadata.gz: 05a36678b1ba07b3e89b1a755e7526facd18cdef04cdfcb9dd1b08224f42bbcc779959c26e869a3ead38e5add649d710e4afc4f0c1f88cdbcd73287360da3f7a
7
- data.tar.gz: 13724331bdafe67729dafd0ca31a863657e125c8bb458fb8a0316aa5a2fba9f466c0e3ff52bc6fbc80a9190639636e726ac8c9940cc92836260b774cc7024ec5
6
+ metadata.gz: 376ab891dc3a0bf92f030dd3b756aa900c8091bfd42b3424ef1c832ecc2ac14384785f96eeadc20764f22ee02315753cba10c4b0aec067294e2175a09754dc60
7
+ data.tar.gz: f60cf65275aaee5e96ebb173cc0718cbc663b3cced951bccf8b417df1029055606be56bdf82617e689130d68687b8ce8f8227acab83314c54715e95e6ca28629
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Not released
4
4
 
5
+ ## 0.17.0
6
+
7
+ - Fix bug with has_many queries due to query method chaining mutating in-place (https://github.com/Beyond-Finance/active_force/pull/10)
8
+
9
+ ## 0.16.0
10
+
11
+ - 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)
12
+ - Support to fetch multi-level associations during eager load (https://github.com/Beyond-Finance/active_force/pull/62)
13
+
5
14
  ## 0.15.1
6
15
 
7
16
  - 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
- super table_name
29
+ @belongs_to_association_mapping = {}
30
+ super custom_table_name || table_name
31
31
  fields sobject.fields
32
32
  end
33
33
 
@@ -42,36 +42,34 @@ module ActiveForce
42
42
  alias_method :all, :to_a
43
43
 
44
44
  def count
45
- super
46
- sfdc_client.query(to_s).first.expr0
45
+ sfdc_client.query(super.to_s).first.expr0
47
46
  end
48
47
 
49
48
  def sum field
50
- super(mappings[field])
51
- sfdc_client.query(to_s).first.expr0
49
+ raise ArgumentError, 'field is required' if field.blank?
50
+ raise ArgumentError, "field '#{field}' does not exist on #{sobject}" unless mappings.key?(field.to_sym)
51
+
52
+ sfdc_client.query(super(mappings.fetch(field.to_sym)).to_s).first.expr0
52
53
  end
53
54
 
54
55
  def limit limit
55
- super
56
- limit == 1 ? to_a.first : self
56
+ limit == 1 ? super.to_a.first : super
57
57
  end
58
58
 
59
59
  def not args=nil, *rest
60
60
  return self if args.nil?
61
+
61
62
  super build_condition args, rest
62
- self
63
63
  end
64
64
 
65
65
  def where args=nil, *rest
66
66
  return self if args.nil?
67
- return clone_self_and_clear_cache.where(args, *rest) if @decorated_records.present?
68
67
  super build_condition args, rest
69
- self
70
68
  end
71
69
 
72
- def select *fields
73
- fields.map! { |field| mappings[field] }
74
- super *fields
70
+ def select *selected_fields
71
+ selected_fields.map! { |field| mappings[field] }
72
+ super *selected_fields
75
73
  end
76
74
 
77
75
  def find!(id)
@@ -93,18 +91,17 @@ module ActiveForce
93
91
  end
94
92
 
95
93
  def includes(*relations)
96
- relations.each do |relation|
97
- association = sobject.associations[relation]
98
- fields Association::EagerLoadProjectionBuilder.build association
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
94
+ includes_query = Association::EagerLoadBuilderForNestedIncludes.build(relations, sobject)
95
+ fields includes_query[:fields]
96
+ association_mapping.merge!(includes_query[:association_mapping])
102
97
  self
103
98
  end
104
99
 
105
100
  def none
106
- @records = []
107
- where(id: '1'*18).where(id: '0'*18)
101
+ clone_and_set_instance_variables(
102
+ records: [],
103
+ conditions: [build_condition(id: '1' * 18), build_condition(id: '0' * 18)]
104
+ )
108
105
  end
109
106
 
110
107
  def loaded?
@@ -208,13 +205,6 @@ module ActiveForce
208
205
  sfdc_client.query(self.to_s)
209
206
  end
210
207
 
211
- def clone_self_and_clear_cache
212
- new_query = self.clone
213
- new_query.instance_variable_set(:@decorated_records, nil)
214
- new_query.instance_variable_set(:@records, nil)
215
- new_query
216
- end
217
-
218
208
  def build_order_by(args)
219
209
  args.map do |arg|
220
210
  case arg
@@ -231,6 +221,5 @@ module ActiveForce
231
221
  def order_type(type)
232
222
  type == :desc ? 'DESC' : 'ASC'
233
223
  end
234
-
235
224
  end
236
225
  end
@@ -16,7 +16,11 @@ module ActiveForce
16
16
  end
17
17
 
18
18
  def default_relationship_name
19
- parent.mappings[foreign_key].gsub(/__c\z/, '__r')
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
- "#{ association.sfdc_association_field }.#{ field }"
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 value
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 mash }
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
@@ -31,33 +31,34 @@ module ActiveForce
31
31
  end
32
32
 
33
33
  def select *columns
34
- @query_fields = columns
35
- self
34
+ clone_and_set_instance_variables(query_fields: columns)
35
+ end
36
+
37
+ def where condition = nil
38
+ new_conditions = @conditions | [condition]
39
+ if new_conditions != @conditions
40
+ clone_and_set_instance_variables({conditions: new_conditions})
41
+ else
42
+ self
43
+ end
36
44
  end
37
45
 
38
46
  def not condition
39
- @conditions << "NOT ((#{ condition.join(') AND (') }))"
40
- self
47
+ condition ? where("NOT ((#{condition.join(') AND (')}))") : self
41
48
  end
42
49
 
43
50
  def or query
44
- @conditions = ["(#{ and_conditions }) OR (#{ query.and_conditions })"]
45
- self
46
- end
51
+ return self unless query
47
52
 
48
- def where condition = nil
49
- @conditions << condition if condition
50
- self
53
+ clone_and_set_instance_variables(conditions: ["(#{and_conditions}) OR (#{query.and_conditions})"])
51
54
  end
52
55
 
53
56
  def order order
54
- @order = order if order
55
- self
57
+ order ? clone_and_set_instance_variables(order: order) : self
56
58
  end
57
59
 
58
60
  def limit size
59
- @size = size if size
60
- self
61
+ size ? clone_and_set_instance_variables(size: size) : self
61
62
  end
62
63
 
63
64
  def limit_value
@@ -65,8 +66,7 @@ module ActiveForce
65
66
  end
66
67
 
67
68
  def offset offset
68
- @offset = offset
69
- self
69
+ clone_and_set_instance_variables(offset: offset)
70
70
  end
71
71
 
72
72
  def offset_value
@@ -74,8 +74,7 @@ module ActiveForce
74
74
  end
75
75
 
76
76
  def find id
77
- where "#{ @table_id } = '#{ id }'"
78
- limit 1
77
+ where("#{ @table_id } = '#{ id }'").limit 1
79
78
  end
80
79
 
81
80
  def first
@@ -87,18 +86,17 @@ module ActiveForce
87
86
  end
88
87
 
89
88
  def join object_query
90
- fields ["(#{ object_query.to_s })"]
91
- self
89
+ chained_query = self.clone
90
+ chained_query.fields ["(#{ object_query.to_s })"]
91
+ chained_query
92
92
  end
93
93
 
94
94
  def count
95
- @query_fields = ["count(Id)"]
96
- self
95
+ clone_and_set_instance_variables(query_fields: ["count(Id)"])
97
96
  end
98
97
 
99
98
  def sum field
100
- @query_fields = ["sum(#{field})"]
101
- self
99
+ clone_and_set_instance_variables(query_fields: ["sum(#{field})"])
102
100
  end
103
101
 
104
102
  protected
@@ -125,5 +123,13 @@ module ActiveForce
125
123
  def build_offset
126
124
  "OFFSET #{ @offset }" if @offset
127
125
  end
126
+
127
+ def clone_and_set_instance_variables instance_variable_hash={}
128
+ clone = self.clone
129
+ clone.instance_variable_set(:@decorated_records, nil)
130
+ clone.instance_variable_set(:@records, nil)
131
+ instance_variable_hash.each { |k,v| clone.instance_variable_set("@#{k.to_s}", v) }
132
+ clone
133
+ end
128
134
  end
129
135
  end
@@ -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
- @attributes.each_value.select { |value| value.is_a?(ActiveModel::Attribute::UserProvidedDefault) }
231
- .map(&:name)
232
- .concat(changed)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveForce
4
- VERSION = '0.15.1'
4
+ VERSION = '0.17.0'
5
5
  end