active_force 0.6.1 → 0.7.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 +7 -0
- data/README.md +43 -17
- data/lib/active_attr/dirty.rb +3 -0
- data/lib/active_force/active_query.rb +32 -2
- data/lib/active_force/association.rb +14 -10
- data/lib/active_force/association/association.rb +26 -11
- data/lib/active_force/association/belongs_to_association.rb +1 -4
- data/lib/active_force/association/eager_load_projection_builder.rb +60 -0
- data/lib/active_force/association/has_many_association.rb +15 -5
- data/lib/active_force/association/relation_model_builder.rb +70 -0
- data/lib/active_force/attribute.rb +30 -0
- data/lib/active_force/mapping.rb +78 -0
- data/lib/active_force/query.rb +2 -8
- data/lib/active_force/sobject.rb +79 -95
- data/lib/active_force/table.rb +6 -2
- data/lib/active_force/version.rb +1 -1
- data/lib/generators/active_force/model/model_generator.rb +1 -0
- data/lib/generators/active_force/model/templates/model.rb.erb +0 -2
- data/spec/active_force/active_query_spec.rb +39 -12
- data/spec/active_force/association/relation_model_builder_spec.rb +62 -0
- data/spec/active_force/association_spec.rb +53 -88
- data/spec/active_force/attribute_spec.rb +27 -0
- data/spec/active_force/callbacks_spec.rb +1 -23
- data/spec/active_force/mapping_spec.rb +18 -0
- data/spec/active_force/query_spec.rb +32 -54
- data/spec/active_force/sobject/includes_spec.rb +290 -0
- data/spec/active_force/sobject/table_name_spec.rb +0 -21
- data/spec/active_force/sobject_spec.rb +212 -29
- data/spec/active_force/table_spec.rb +0 -3
- data/spec/fixtures/sobject/single_sobject_hash.yml +2 -0
- data/spec/spec_helper.rb +10 -4
- data/spec/support/restforce_factories.rb +9 -0
- data/spec/support/sobjects.rb +97 -0
- data/spec/support/whizbang.rb +25 -7
- metadata +18 -2
@@ -0,0 +1,70 @@
|
|
1
|
+
module ActiveForce
|
2
|
+
module Association
|
3
|
+
class RelationModelBuilder
|
4
|
+
class << self
|
5
|
+
def build(association, value)
|
6
|
+
new(association, value).build_relation_model
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(association, value)
|
11
|
+
@association = association
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_relation_model
|
16
|
+
klass = resolve_class
|
17
|
+
klass.new(@association, @value).call
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def resolve_class
|
23
|
+
association_builder = @value.class.name.gsub('::', '_')
|
24
|
+
ActiveForce::Association.const_get "BuildFrom#{association_builder}"
|
25
|
+
rescue NameError
|
26
|
+
raise "Don't know how to build relation from #{@value.class.name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class AbstractBuildFrom
|
31
|
+
attr_reader :association, :value
|
32
|
+
|
33
|
+
def initialize(association, value)
|
34
|
+
@association = association
|
35
|
+
@value = value
|
36
|
+
end
|
37
|
+
|
38
|
+
def call
|
39
|
+
raise "Must implement #{self.class.name}#call"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class BuildFromHash < AbstractBuildFrom
|
44
|
+
def call
|
45
|
+
association.build value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class BuildFromArray < AbstractBuildFrom
|
50
|
+
def call
|
51
|
+
value.map { |mash| association.build mash }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class BuildFromNilClass < AbstractBuildFrom
|
56
|
+
def call
|
57
|
+
association.is_a?(BelongsToAssociation) ? nil : []
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class BuildFromRestforce_SObject < BuildFromHash
|
62
|
+
end
|
63
|
+
|
64
|
+
class BuildFromRestforce_Mash < BuildFromHash
|
65
|
+
end
|
66
|
+
|
67
|
+
class BuildFromRestforce_Collection < BuildFromArray
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActiveForce
|
2
|
+
class Attribute
|
3
|
+
|
4
|
+
attr_accessor :local_name, :sfdc_name, :as
|
5
|
+
|
6
|
+
def initialize name, options = {}
|
7
|
+
self.local_name = name
|
8
|
+
self.sfdc_name = options[:sfdc_name] || options[:from] || default_api_name
|
9
|
+
self.as = options[:as] || :string
|
10
|
+
end
|
11
|
+
|
12
|
+
def value_for_hash value
|
13
|
+
case as
|
14
|
+
when :multi_picklist
|
15
|
+
value.reject(&:empty?).join(';')
|
16
|
+
else
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
###
|
24
|
+
# Transforms +attribute+ to the conventional Salesforce API name.
|
25
|
+
#
|
26
|
+
def default_api_name
|
27
|
+
local_name.to_s.split('_').map(&:capitalize).join('_') << '__c'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'active_force/attribute'
|
2
|
+
require 'active_force/table'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module ActiveForce
|
6
|
+
class Mapping
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
STRINGLIKE_TYPES = [
|
10
|
+
nil, :string, :base64, :byte, :ID, :reference, :currency, :textarea,
|
11
|
+
:phone, :url, :email, :combobox, :picklist, :multipicklist, :anyType,
|
12
|
+
:location
|
13
|
+
]
|
14
|
+
|
15
|
+
def_delegators :table, :custom_table?, :table_name
|
16
|
+
|
17
|
+
def initialize model
|
18
|
+
@model = model
|
19
|
+
end
|
20
|
+
|
21
|
+
def mappings
|
22
|
+
Hash[fields.map { |field, attr| [field, attr.sfdc_name] }]
|
23
|
+
end
|
24
|
+
|
25
|
+
def sfdc_names
|
26
|
+
mappings.values
|
27
|
+
end
|
28
|
+
|
29
|
+
def field name, options
|
30
|
+
fields.merge!({ name => ActiveForce::Attribute.new(name, options) })
|
31
|
+
end
|
32
|
+
|
33
|
+
def table
|
34
|
+
@table ||= ActiveForce::Table.new @model
|
35
|
+
end
|
36
|
+
|
37
|
+
def translate_to_sf attributes
|
38
|
+
attrs = attributes.map do |attribute, value|
|
39
|
+
attr = fields[attribute.to_sym]
|
40
|
+
[attr.sfdc_name, attr.value_for_hash(value)]
|
41
|
+
end
|
42
|
+
Hash[attrs]
|
43
|
+
end
|
44
|
+
|
45
|
+
def translate_value value, field_name
|
46
|
+
return value unless !!field_name
|
47
|
+
typecast_value value.to_s, fields[field_name].as
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def fields
|
54
|
+
@fields ||= {}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Handles Salesforce FieldTypes as described here:
|
58
|
+
# http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_describesobjects_describesobjectresult.htm#i1427700
|
59
|
+
def typecast_value value, type
|
60
|
+
case type
|
61
|
+
when *STRINGLIKE_TYPES
|
62
|
+
value
|
63
|
+
when :boolean
|
64
|
+
!['false','0','f'].include? value.downcase
|
65
|
+
when :int
|
66
|
+
value.to_i
|
67
|
+
when :double, :percent
|
68
|
+
value.to_f
|
69
|
+
when :date
|
70
|
+
Date.parse value
|
71
|
+
when :datetime
|
72
|
+
DateTime.parse value
|
73
|
+
else
|
74
|
+
value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/active_force/query.rb
CHANGED
@@ -86,19 +86,13 @@ module ActiveForce
|
|
86
86
|
self
|
87
87
|
end
|
88
88
|
|
89
|
-
def options args
|
90
|
-
where args[:where]
|
91
|
-
limit args[:limit]
|
92
|
-
order args[:order]
|
93
|
-
end
|
94
|
-
|
95
89
|
protected
|
96
90
|
def build_select
|
97
|
-
@query_fields.uniq.join(', ')
|
91
|
+
@query_fields.compact.uniq.join(', ')
|
98
92
|
end
|
99
93
|
|
100
94
|
def build_where
|
101
|
-
"WHERE #{ @conditions.join(' AND ') }" unless @conditions.empty?
|
95
|
+
"WHERE (#{ @conditions.join(') AND (') })" unless @conditions.empty?
|
102
96
|
end
|
103
97
|
|
104
98
|
def build_limit
|
data/lib/active_force/sobject.rb
CHANGED
@@ -3,17 +3,19 @@ require 'active_attr'
|
|
3
3
|
require 'active_attr/dirty'
|
4
4
|
require 'active_force/active_query'
|
5
5
|
require 'active_force/association'
|
6
|
-
require 'active_force/
|
6
|
+
require 'active_force/mapping'
|
7
7
|
require 'yaml'
|
8
8
|
require 'forwardable'
|
9
9
|
require 'logger'
|
10
10
|
require 'restforce'
|
11
11
|
|
12
12
|
module ActiveForce
|
13
|
+
class RecordInvalid < StandardError;end
|
14
|
+
|
13
15
|
class SObject
|
14
16
|
include ActiveAttr::Model
|
15
17
|
include ActiveAttr::Dirty
|
16
|
-
|
18
|
+
extend ActiveForce::Association
|
17
19
|
extend ActiveModel::Callbacks
|
18
20
|
|
19
21
|
define_model_callbacks :save, :create, :update
|
@@ -22,21 +24,11 @@ module ActiveForce
|
|
22
24
|
|
23
25
|
class << self
|
24
26
|
extend Forwardable
|
25
|
-
def_delegators :query, :where, :first, :last, :all, :find, :find_by, :count
|
26
|
-
def_delegators :table, :
|
27
|
+
def_delegators :query, :where, :first, :last, :all, :find, :find_by, :count, :includes, :limit, :order, :select
|
28
|
+
def_delegators :mapping, :table, :table_name, :custom_table?, :mappings
|
27
29
|
|
28
30
|
private
|
29
31
|
|
30
|
-
###
|
31
|
-
# Transforms +attribute+ to the conventional Salesforce API name.
|
32
|
-
#
|
33
|
-
# Example:
|
34
|
-
# > default_api_name :some_attribute
|
35
|
-
# => "Some_Attribute__c"
|
36
|
-
def default_api_name(attribute)
|
37
|
-
String(attribute).split('_').map(&:capitalize).join('_') << '__c'
|
38
|
-
end
|
39
|
-
|
40
32
|
###
|
41
33
|
# Provide each subclass with a default id field. Can be overridden
|
42
34
|
# in the subclass if needed
|
@@ -45,29 +37,23 @@ module ActiveForce
|
|
45
37
|
end
|
46
38
|
end
|
47
39
|
|
48
|
-
|
49
|
-
|
50
|
-
def self.table_name
|
51
|
-
table.name
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.table
|
55
|
-
@table ||= ActiveForce::Table.new name
|
40
|
+
def self.mapping
|
41
|
+
@mapping ||= ActiveForce::Mapping.new name
|
56
42
|
end
|
57
43
|
|
58
44
|
def self.fields
|
59
|
-
|
45
|
+
mapping.sfdc_names
|
60
46
|
end
|
61
47
|
|
62
48
|
def self.query
|
63
49
|
ActiveForce::ActiveQuery.new self
|
64
50
|
end
|
65
51
|
|
66
|
-
def self.build
|
67
|
-
return unless
|
52
|
+
def self.build mash
|
53
|
+
return unless mash
|
68
54
|
sobject = new
|
69
|
-
|
70
|
-
sobject
|
55
|
+
mash.each do |column, sf_value|
|
56
|
+
sobject.write_value column, sf_value
|
71
57
|
end
|
72
58
|
sobject.changed_attributes.clear
|
73
59
|
sobject
|
@@ -75,35 +61,42 @@ module ActiveForce
|
|
75
61
|
|
76
62
|
def update_attributes! attributes = {}
|
77
63
|
assign_attributes attributes
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
64
|
+
validate!
|
65
|
+
run_callbacks :save do
|
66
|
+
run_callbacks :update do
|
67
|
+
sfdc_client.update! table_name, attributes_for_sfdb
|
68
|
+
changed_attributes.clear
|
69
|
+
end
|
70
|
+
end
|
71
|
+
true
|
82
72
|
end
|
83
73
|
|
74
|
+
alias_method :update!, :update_attributes!
|
75
|
+
|
84
76
|
def update_attributes attributes = {}
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
rescue Faraday::Error::ClientError => error
|
89
|
-
logger_output __method__
|
77
|
+
update_attributes! attributes
|
78
|
+
rescue Faraday::Error::ClientError, RecordInvalid => error
|
79
|
+
handle_save_error error
|
90
80
|
end
|
91
81
|
|
92
82
|
alias_method :update, :update_attributes
|
93
83
|
|
94
84
|
def create!
|
95
|
-
|
96
|
-
|
97
|
-
|
85
|
+
validate!
|
86
|
+
run_callbacks :save do
|
87
|
+
run_callbacks :create do
|
88
|
+
self.id = sfdc_client.create! table_name, attributes_for_sfdb
|
89
|
+
changed_attributes.clear
|
90
|
+
end
|
91
|
+
end
|
98
92
|
self
|
99
93
|
end
|
100
94
|
|
101
95
|
def create
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
logger_output __method__
|
96
|
+
create!
|
97
|
+
rescue Faraday::Error::ClientError, RecordInvalid => error
|
98
|
+
handle_save_error error
|
99
|
+
self
|
107
100
|
end
|
108
101
|
|
109
102
|
def destroy
|
@@ -111,27 +104,27 @@ module ActiveForce
|
|
111
104
|
end
|
112
105
|
|
113
106
|
def self.create args
|
114
|
-
new(args).
|
107
|
+
new(args).create
|
115
108
|
end
|
116
109
|
|
117
110
|
def self.create! args
|
118
|
-
new(args).
|
111
|
+
new(args).create!
|
119
112
|
end
|
120
113
|
|
121
|
-
def save
|
114
|
+
def save!
|
122
115
|
run_callbacks :save do
|
123
116
|
if persisted?
|
124
|
-
update
|
117
|
+
!!update!
|
125
118
|
else
|
126
|
-
create
|
119
|
+
!!create!
|
127
120
|
end
|
128
121
|
end
|
129
122
|
end
|
130
123
|
|
131
|
-
def save
|
132
|
-
save
|
133
|
-
rescue Faraday::Error::ClientError => error
|
134
|
-
|
124
|
+
def save
|
125
|
+
save!
|
126
|
+
rescue Faraday::Error::ClientError, RecordInvalid => error
|
127
|
+
handle_save_error error
|
135
128
|
end
|
136
129
|
|
137
130
|
def to_param
|
@@ -143,14 +136,8 @@ module ActiveForce
|
|
143
136
|
end
|
144
137
|
|
145
138
|
def self.field field_name, args = {}
|
146
|
-
|
147
|
-
|
148
|
-
mappings[field_name] = args[:from]
|
149
|
-
attribute field_name, sf_type: args[:as]
|
150
|
-
end
|
151
|
-
|
152
|
-
def self.mappings
|
153
|
-
@mappings ||= {}
|
139
|
+
mapping.field field_name, args
|
140
|
+
attribute field_name
|
154
141
|
end
|
155
142
|
|
156
143
|
def reload
|
@@ -161,50 +148,47 @@ module ActiveForce
|
|
161
148
|
self
|
162
149
|
end
|
163
150
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
151
|
+
def write_value column, value
|
152
|
+
if association = self.class.find_association(column)
|
153
|
+
field = association.relation_name
|
154
|
+
value = Association::RelationModelBuilder.build(association, value)
|
155
|
+
else
|
156
|
+
field = mappings.invert[column]
|
157
|
+
value = self.class.mapping.translate_value value, field unless value.nil?
|
158
|
+
end
|
159
|
+
send "#{field}=", value if field
|
168
160
|
end
|
169
161
|
|
170
|
-
|
171
|
-
logger = Logger.new(STDOUT)
|
172
|
-
logger.info("[SFDC] [#{self.class.model_name}] [#{self.class.table_name}] Error while #{ action }, params: #{hash}, error: #{error.inspect}")
|
173
|
-
errors[:base] << error.message
|
174
|
-
false
|
175
|
-
end
|
162
|
+
private
|
176
163
|
|
177
|
-
def
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
end
|
183
|
-
attrs << ['Id', id]
|
184
|
-
else
|
185
|
-
attrs = mappings.map do |attr, sf_field|
|
186
|
-
value = read_value(attr)
|
187
|
-
[sf_field, value] unless value.nil?
|
188
|
-
end
|
164
|
+
def validate!
|
165
|
+
unless valid?
|
166
|
+
raise RecordInvalid.new(
|
167
|
+
"Validation failed: #{errors.full_messages.join(', ')}"
|
168
|
+
)
|
189
169
|
end
|
190
|
-
Hash[attrs.compact]
|
191
170
|
end
|
192
171
|
|
193
|
-
def
|
194
|
-
|
172
|
+
def handle_save_error error
|
173
|
+
return false if error.class == RecordInvalid
|
174
|
+
logger_output __method__, error, attributes
|
195
175
|
end
|
196
176
|
|
197
|
-
def
|
198
|
-
|
199
|
-
when :multi_picklist
|
200
|
-
attribute(field.to_s).reject(&:empty?).join(';')
|
201
|
-
else
|
202
|
-
attribute(field.to_s)
|
203
|
-
end
|
177
|
+
def association_cache
|
178
|
+
@association_cache ||= {}
|
204
179
|
end
|
205
180
|
|
206
|
-
def
|
207
|
-
|
181
|
+
def logger_output action, exception, params = {}
|
182
|
+
logger = Logger.new(STDOUT)
|
183
|
+
logger.info("[SFDC] [#{self.class.model_name}] [#{self.class.table_name}] Error while #{ action }, params: #{params}, error: #{exception.inspect}")
|
184
|
+
errors[:base] << exception.message
|
185
|
+
false
|
186
|
+
end
|
187
|
+
|
188
|
+
def attributes_for_sfdb
|
189
|
+
attrs = self.class.mapping.translate_to_sf(attributes_and_changes)
|
190
|
+
attrs.merge!({'Id' => id }) if persisted?
|
191
|
+
attrs
|
208
192
|
end
|
209
193
|
|
210
194
|
def self.picklist field
|