active_force 0.15.1 → 0.17.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: 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