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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +15 -0
- data/lib/active_force/active_query.rb +21 -32
- data/lib/active_force/association/belongs_to_association.rb +5 -1
- data/lib/active_force/association/eager_load_builder_for_nested_includes.rb +84 -0
- data/lib/active_force/association/eager_load_projection_builder.rb +15 -8
- data/lib/active_force/association/relation_model_builder.rb +10 -8
- data/lib/active_force/association.rb +1 -0
- data/lib/active_force/query.rb +30 -24
- data/lib/active_force/sobject.rb +10 -6
- data/lib/active_force/version.rb +1 -1
- data/spec/active_force/active_query_spec.rb +101 -59
- data/spec/active_force/association_spec.rb +11 -1
- data/spec/active_force/query_spec.rb +60 -5
- data/spec/active_force/sobject/includes_spec.rb +325 -0
- data/spec/active_force/sobject_spec.rb +69 -6
- data/spec/support/sobjects.rb +41 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 741f33a6c27d6bc11eaaa7655c2a7fb2ecc772f93d298e608866840b82e94321
|
4
|
+
data.tar.gz: 465abe2c572e35dc61817ab1bf8ac1d6d0e35e6f26eb2e933af3ed1781c2c68a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
51
|
-
|
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 *
|
73
|
-
|
74
|
-
super *
|
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
|
97
|
-
|
98
|
-
|
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
|
-
|
107
|
-
|
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.
|
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
|
-
"#{
|
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
|
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
|
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
|
data/lib/active_force/query.rb
CHANGED
@@ -31,33 +31,34 @@ module ActiveForce
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def select *columns
|
34
|
-
|
35
|
-
|
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
|
-
|
40
|
-
self
|
47
|
+
condition ? where("NOT ((#{condition.join(') AND (')}))") : self
|
41
48
|
end
|
42
49
|
|
43
50
|
def or query
|
44
|
-
|
45
|
-
self
|
46
|
-
end
|
51
|
+
return self unless query
|
47
52
|
|
48
|
-
|
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
|
-
|
55
|
-
self
|
57
|
+
order ? clone_and_set_instance_variables(order: order) : self
|
56
58
|
end
|
57
59
|
|
58
60
|
def limit size
|
59
|
-
|
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
|
-
|
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
|
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
|
-
|
91
|
-
|
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
|
-
|
96
|
-
self
|
95
|
+
clone_and_set_instance_variables(query_fields: ["count(Id)"])
|
97
96
|
end
|
98
97
|
|
99
98
|
def sum field
|
100
|
-
|
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
|
data/lib/active_force/sobject.rb
CHANGED
@@ -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
|
-
|
231
|
-
|
232
|
-
|
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
|
data/lib/active_force/version.rb
CHANGED