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