activeforce 1.5.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.
- data/Gemfile +15 -0
- data/Gemfile.lock +128 -0
- data/LICENSE.txt +20 -0
- data/README.md +112 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/activeforce.gemspec +151 -0
- data/app/models/salesforce/account.rb +4 -0
- data/app/models/salesforce/activity_history.rb +4 -0
- data/app/models/salesforce/approval.rb +4 -0
- data/app/models/salesforce/campaign.rb +4 -0
- data/app/models/salesforce/campaign_feed.rb +4 -0
- data/app/models/salesforce/campaign_member.rb +4 -0
- data/app/models/salesforce/case.rb +4 -0
- data/app/models/salesforce/case_comment.rb +4 -0
- data/app/models/salesforce/case_contact_role.rb +4 -0
- data/app/models/salesforce/case_feed.rb +4 -0
- data/app/models/salesforce/case_history.rb +4 -0
- data/app/models/salesforce/case_share.rb +4 -0
- data/app/models/salesforce/case_solution.rb +4 -0
- data/app/models/salesforce/case_status.rb +4 -0
- data/app/models/salesforce/case_team_member.rb +4 -0
- data/app/models/salesforce/community.rb +4 -0
- data/app/models/salesforce/contact.rb +4 -0
- data/app/models/salesforce/contact_feed.rb +4 -0
- data/app/models/salesforce/contact_history.rb +4 -0
- data/app/models/salesforce/contract.rb +4 -0
- data/app/models/salesforce/document.rb +4 -0
- data/app/models/salesforce/event.rb +4 -0
- data/app/models/salesforce/feed_item.rb +4 -0
- data/app/models/salesforce/group.rb +4 -0
- data/app/models/salesforce/group_member.rb +4 -0
- data/app/models/salesforce/idea.rb +4 -0
- data/app/models/salesforce/lead.rb +4 -0
- data/app/models/salesforce/lead_status.rb +4 -0
- data/app/models/salesforce/name.rb +4 -0
- data/app/models/salesforce/note.rb +4 -0
- data/app/models/salesforce/open_activity.rb +4 -0
- data/app/models/salesforce/opportunity.rb +4 -0
- data/app/models/salesforce/organization.rb +4 -0
- data/app/models/salesforce/partner.rb +4 -0
- data/app/models/salesforce/period.rb +4 -0
- data/app/models/salesforce/product2.rb +4 -0
- data/app/models/salesforce/product2_feed.rb +4 -0
- data/app/models/salesforce/profile.rb +4 -0
- data/app/models/salesforce/quote.rb +4 -0
- data/app/models/salesforce/solution.rb +4 -0
- data/app/models/salesforce/task.rb +4 -0
- data/app/models/salesforce/task_feed.rb +4 -0
- data/app/models/salesforce/task_priority.rb +4 -0
- data/app/models/salesforce/task_status.rb +4 -0
- data/app/models/salesforce/user.rb +4 -0
- data/app/models/salesforce/user_role.rb +4 -0
- data/app/models/salesforce/vote.rb +4 -0
- data/lib/activeforce.rb +29 -0
- data/lib/salesforce/attributes.rb +13 -0
- data/lib/salesforce/authentication.rb +25 -0
- data/lib/salesforce/base.rb +240 -0
- data/lib/salesforce/bulk/batch.rb +77 -0
- data/lib/salesforce/bulk/job.rb +103 -0
- data/lib/salesforce/bulk/operations.rb +25 -0
- data/lib/salesforce/bulk/update_job.rb +24 -0
- data/lib/salesforce/column.rb +98 -0
- data/lib/salesforce/columns.rb +60 -0
- data/lib/salesforce/config.rb +110 -0
- data/lib/salesforce/connection.rb +33 -0
- data/lib/salesforce/connection/async.rb +36 -0
- data/lib/salesforce/connection/conversion.rb +37 -0
- data/lib/salesforce/connection/http_methods.rb +72 -0
- data/lib/salesforce/connection/rest_api.rb +52 -0
- data/lib/salesforce/connection/soap_api.rb +74 -0
- data/lib/salesforce/engine.rb +6 -0
- data/lib/salesforce/errors.rb +33 -0
- data/test/salesforce/authentication_test.rb +52 -0
- data/test/salesforce/base_test.rb +440 -0
- data/test/salesforce/bulk/batch_test.rb +79 -0
- data/test/salesforce/bulk/update_job_test.rb +125 -0
- data/test/salesforce/column_test.rb +115 -0
- data/test/salesforce/config_test.rb +163 -0
- data/test/salesforce/connection/async_test.rb +138 -0
- data/test/salesforce/connection/http_methods_test.rb +242 -0
- data/test/salesforce/connection/rest_api_test.rb +61 -0
- data/test/salesforce/connection/soap_api_test.rb +148 -0
- data/test/salesforce/connection_test.rb +148 -0
- data/test/test_helper.rb +79 -0
- metadata +295 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module Salesforce
|
2
|
+
module Bulk
|
3
|
+
class Batch
|
4
|
+
Result = Struct.new :id, :success, :created, :error
|
5
|
+
include ::Salesforce::Attributes
|
6
|
+
|
7
|
+
attr_accessor :id, :job, :number_records_failed, :number_records_processed, :state, :state_message, :total_time_processed, :filename, :csv
|
8
|
+
|
9
|
+
include Blockenspiel::DSL
|
10
|
+
|
11
|
+
def initialize(job)
|
12
|
+
self.job = job
|
13
|
+
self.filename = temporary_csv_file
|
14
|
+
self.csv = FasterCSV.open(self.filename, 'w+')
|
15
|
+
self.csv << csv_header
|
16
|
+
end
|
17
|
+
|
18
|
+
def record(record)
|
19
|
+
if record.is_a?(Hash)
|
20
|
+
self.csv << ordered_values(record)
|
21
|
+
else
|
22
|
+
self.csv << ordered_values(record.attributes)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def ordered_values(record)
|
27
|
+
job.csv_columns.map do |col|
|
28
|
+
raw_value = record[col.name.to_sym]
|
29
|
+
Column.to_csv_value Column.typecast(col.type, raw_value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def create!
|
34
|
+
self.csv.close
|
35
|
+
response = ::Salesforce.connection.async_post("job/#{job.id}/batch", File.read(self.filename), :format => :xml, :content_type => 'text/csv')
|
36
|
+
assign_attributes!(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_status
|
40
|
+
return state if completed?
|
41
|
+
response = ::Salesforce.connection.async_get("job/#{job.id}/batch/#{id}", :format => :xml)
|
42
|
+
self.state = response[:state]
|
43
|
+
end
|
44
|
+
|
45
|
+
def results
|
46
|
+
parse_csv_results ::Salesforce.connection.async_get("job/#{job.id}/batch/#{id}/result")
|
47
|
+
end
|
48
|
+
|
49
|
+
[ :queued, :in_progress, :completed, :failed, :not_processed ].each do |status|
|
50
|
+
define_method "#{status}?" do
|
51
|
+
self.state == status.to_s.titleize
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def temporary_csv_file
|
56
|
+
if Object.const_defined?(:Rails)
|
57
|
+
Rails.root.join('tmp', 'files', "#{ Time.now.to_i}#{rand(10000)}.csv")
|
58
|
+
else
|
59
|
+
File.join("/tmp/#{ Time.now.to_i}#{rand(10000)}.csv")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def parse_csv_results(results)
|
66
|
+
parsed_results = FasterCSV.parse(results)
|
67
|
+
parsed_results[1..-1].map { |row| Result.new(*row) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def csv_header
|
71
|
+
self.job.csv_columns.map(&:original_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Salesforce
|
2
|
+
module Bulk
|
3
|
+
class Job
|
4
|
+
include ::Salesforce::Attributes
|
5
|
+
|
6
|
+
include Blockenspiel::DSL
|
7
|
+
|
8
|
+
attr_accessor :id, :operation, :object, :state, :concurrency_mode, :content_type, :number_of_batches_queued,
|
9
|
+
:number_batches_in_progress, :number_batches_completed, :number_batches_failed, :number_batches_total,
|
10
|
+
:number_records_processed, :number_retries, :object_type, :batches, :columns
|
11
|
+
|
12
|
+
[ :open, :closed, :aborted, :failed ].each do |status|
|
13
|
+
define_method "#{status}?" do
|
14
|
+
self.state == status.to_s.titleize
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(object_type, operation, columns = :all)
|
19
|
+
self.object_type = object_type
|
20
|
+
self.object = object_type.table_name
|
21
|
+
self.operation = operation.to_s.downcase
|
22
|
+
self.parallel!
|
23
|
+
self.batches = []
|
24
|
+
self.columns = columns
|
25
|
+
end
|
26
|
+
|
27
|
+
def parallel!
|
28
|
+
self.concurrency_mode = "Parallel"
|
29
|
+
end
|
30
|
+
|
31
|
+
def serial!
|
32
|
+
self.concurrency_mode = "Serial"
|
33
|
+
end
|
34
|
+
|
35
|
+
def batch(&block)
|
36
|
+
Batch.new(self).tap do |batch|
|
37
|
+
Blockenspiel.invoke(block, batch)
|
38
|
+
self.batches << batch
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def process!
|
43
|
+
create_job!
|
44
|
+
create_batches!
|
45
|
+
close_job!
|
46
|
+
end
|
47
|
+
|
48
|
+
def completed?
|
49
|
+
self.batches.each(&:update_status)
|
50
|
+
self.batches.all? { |batch| batch.completed? || batch.failed? }
|
51
|
+
end
|
52
|
+
|
53
|
+
def results
|
54
|
+
self.batches.map(&:results).flatten
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def create_job!
|
60
|
+
response = ::Salesforce.connection.async_post("job", create_job_xml, :format => :xml)
|
61
|
+
assign_attributes!(response)
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_batches!
|
65
|
+
batches.each(&:create!)
|
66
|
+
end
|
67
|
+
|
68
|
+
def close_job!
|
69
|
+
response = ::Salesforce.connection.async_post("job/#{id}", close_job_xml, :format => :xml)
|
70
|
+
assign_attributes!(response)
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_job_xml
|
74
|
+
job_xml do |job_info|
|
75
|
+
job_info.operation self.operation
|
76
|
+
job_info.object self.object
|
77
|
+
job_info.contentType "CSV"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def close_job_xml
|
82
|
+
job_xml do |job_info|
|
83
|
+
job_info.state "Closed"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def job_xml(&block)
|
88
|
+
xml = Builder::XmlMarkup.new(:indent => 2)
|
89
|
+
xml.instruct!
|
90
|
+
xml.jobInfo :xmlns => "http://www.force.com/2009/06/asyncapi/dataload" do |job_info|
|
91
|
+
block.call(job_info)
|
92
|
+
end
|
93
|
+
xml.target!
|
94
|
+
end
|
95
|
+
|
96
|
+
def csv_header
|
97
|
+
csv_columns.map(&:original_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Salesforce
|
2
|
+
module Bulk
|
3
|
+
module Operations
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
# Create a bulk update job
|
8
|
+
# job = Salesforce::Account.bulk_update do
|
9
|
+
# batch do
|
10
|
+
# record account_1
|
11
|
+
# record account_2
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
|
16
|
+
def bulk_update(columns = [], &block)
|
17
|
+
UpdateJob.new(self, columns).tap do |job|
|
18
|
+
Blockenspiel.invoke(block, job)
|
19
|
+
job.process!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Salesforce
|
2
|
+
module Bulk
|
3
|
+
class UpdateJob < Job
|
4
|
+
|
5
|
+
def initialize(object_type, columns = :all)
|
6
|
+
super(object_type, 'update', columns)
|
7
|
+
end
|
8
|
+
|
9
|
+
def csv_columns
|
10
|
+
[ object_type.columns.id_column ] + if columns.blank? || columns == :all
|
11
|
+
object_type.columns.updateable
|
12
|
+
else
|
13
|
+
columns.map do |col|
|
14
|
+
sf_col = object_type.columns.find { |scol| scol.name == col.to_s }
|
15
|
+
raise UnrecognizedColumn.new("#{col} is not a valid column.") unless sf_col
|
16
|
+
sf_col
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Salesforce
|
2
|
+
class Column
|
3
|
+
attr_accessor :name, :original_name, :createable, :updateable, :type
|
4
|
+
|
5
|
+
def initialize(field)
|
6
|
+
self.original_name = field["name"]
|
7
|
+
self.name = field["name"].gsub(/\_\_c$/, '').underscore
|
8
|
+
self.type = field["type"].to_sym
|
9
|
+
self.createable = field['createable']
|
10
|
+
self.updateable = field["updateable"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def createable?
|
14
|
+
createable
|
15
|
+
end
|
16
|
+
|
17
|
+
def updateable?
|
18
|
+
updateable
|
19
|
+
end
|
20
|
+
|
21
|
+
def editable?
|
22
|
+
createable? || updateable?
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def self.to_soql_value(obj)
|
27
|
+
case (obj)
|
28
|
+
when Date
|
29
|
+
obj.strftime("%Y-%m-%d")
|
30
|
+
when TrueClass
|
31
|
+
'TRUE'
|
32
|
+
when FalseClass
|
33
|
+
'FALSE'
|
34
|
+
when Time
|
35
|
+
obj.xmlschema
|
36
|
+
when nil
|
37
|
+
'NULL'
|
38
|
+
when Numeric
|
39
|
+
"#{obj.to_s}"
|
40
|
+
when Array
|
41
|
+
"(#{obj.map { |sobj| to_soql_value(sobj) }.join(',')})"
|
42
|
+
else
|
43
|
+
"'#{obj.to_s}'"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.to_csv_value(obj)
|
48
|
+
case (obj)
|
49
|
+
when Date;
|
50
|
+
obj.strftime("%Y-%m-%d")
|
51
|
+
when TrueClass;
|
52
|
+
'TRUE'
|
53
|
+
when FalseClass;
|
54
|
+
'FALSE'
|
55
|
+
when Time;
|
56
|
+
obj.xmlschema
|
57
|
+
else
|
58
|
+
"#{obj.to_s}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.typecast(type, value)
|
63
|
+
case (type)
|
64
|
+
when :id, :reference
|
65
|
+
if Config.use_full_length_ids?
|
66
|
+
value
|
67
|
+
else
|
68
|
+
value.to_s.size == 15 ? value : value.to_s[0..14]
|
69
|
+
end
|
70
|
+
when :date
|
71
|
+
begin
|
72
|
+
Date.parse(value);
|
73
|
+
rescue
|
74
|
+
value if value.is_a?(Date)
|
75
|
+
end
|
76
|
+
when :datetime
|
77
|
+
begin
|
78
|
+
Time.parse(value)
|
79
|
+
rescue
|
80
|
+
value if value.is_a?(Time)
|
81
|
+
end
|
82
|
+
when :double
|
83
|
+
begin
|
84
|
+
BigDecimal(value.to_s)
|
85
|
+
rescue
|
86
|
+
value if value.is_a?(Numeric)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def ==(other)
|
94
|
+
return false unless other
|
95
|
+
self.name == other.name && self.original_name == other.original_name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Salesforce
|
2
|
+
class Columns
|
3
|
+
include Enumerable
|
4
|
+
attr_accessor :by_name, :by_original_name, :table_name
|
5
|
+
|
6
|
+
def initialize(table_name)
|
7
|
+
self.table_name = table_name
|
8
|
+
fields = Connection.fields(table_name)
|
9
|
+
self.by_name = {}
|
10
|
+
self.by_original_name = {}
|
11
|
+
fields.each do |field|
|
12
|
+
column = Column.new(field)
|
13
|
+
by_name[column.name] = column
|
14
|
+
by_original_name[column.original_name] = column
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each(&block)
|
19
|
+
all.each(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def all
|
23
|
+
@all ||= by_name.values.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
def editable
|
27
|
+
@editable ||= select(&:editable?)
|
28
|
+
end
|
29
|
+
|
30
|
+
def createable
|
31
|
+
select(&:createable?)
|
32
|
+
end
|
33
|
+
|
34
|
+
def updateable
|
35
|
+
select(&:updateable?)
|
36
|
+
end
|
37
|
+
|
38
|
+
def id_column
|
39
|
+
find { |col| col.name.to_sym == :id }
|
40
|
+
end
|
41
|
+
|
42
|
+
def names
|
43
|
+
map(&:name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def soql_selector
|
47
|
+
@soql_selector ||= by_original_name.keys.sort.join(',')
|
48
|
+
end
|
49
|
+
|
50
|
+
def ==(other)
|
51
|
+
other && self.all.map(&:name) == other.all.map(&:name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_by_name(name)
|
55
|
+
column = by_name[name.to_s]
|
56
|
+
raise ColumnNotFound.new(name, table_name) unless column
|
57
|
+
column
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Salesforce
|
2
|
+
class Config
|
3
|
+
|
4
|
+
DEFAULT_API_VERSION = "22.0"
|
5
|
+
|
6
|
+
include Blockenspiel::DSL
|
7
|
+
include Blockenspiel::DSLSetupMethods
|
8
|
+
|
9
|
+
dsl_attr_accessor :session_id, :server_instance, :user_id, :soap_endpoint_url
|
10
|
+
|
11
|
+
[
|
12
|
+
:username, :password, :api_version, :use_sandbox?, :use_full_length_ids?,
|
13
|
+
:login_url, :session_id, :server_instance, :soap_endpoint_url, :soap_enterprise_namespace,
|
14
|
+
:user_id, :server_url, :server_host, :async_url, :configured?, :on_login_failure ].each do |method_name|
|
15
|
+
eval <<-RUBY
|
16
|
+
def self.#{method_name}
|
17
|
+
instance.#{method_name}
|
18
|
+
end
|
19
|
+
RUBY
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.instance
|
23
|
+
@instance ||= new
|
24
|
+
end
|
25
|
+
|
26
|
+
def username(*args, &block)
|
27
|
+
if block.present?
|
28
|
+
@username = Proc.new { block.call }
|
29
|
+
elsif args.present?
|
30
|
+
@username = args.first
|
31
|
+
elsif @username.respond_to?(:call)
|
32
|
+
@username.call
|
33
|
+
else
|
34
|
+
@username
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def password(*args, &block)
|
39
|
+
if block.present?
|
40
|
+
@password = Proc.new { block.call }
|
41
|
+
elsif args.present?
|
42
|
+
@password = args.first
|
43
|
+
elsif @password.respond_to?(:call)
|
44
|
+
@password.call
|
45
|
+
else
|
46
|
+
@password
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def api_version(val = nil)
|
51
|
+
if val
|
52
|
+
@api_version = val.to_f.to_s
|
53
|
+
else
|
54
|
+
@api_version || DEFAULT_API_VERSION
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def use_sandbox?
|
59
|
+
@use_sandbox || false
|
60
|
+
end
|
61
|
+
|
62
|
+
def use_full_length_ids?
|
63
|
+
@use_full_length_ids || false
|
64
|
+
end
|
65
|
+
|
66
|
+
def use_full_length_ids
|
67
|
+
@use_full_length_ids = true
|
68
|
+
end
|
69
|
+
|
70
|
+
def use_sandbox
|
71
|
+
@use_sandbox = true
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_login_failure(&block)
|
75
|
+
if block.present?
|
76
|
+
@on_login_failure = Proc.new { block.call }
|
77
|
+
else
|
78
|
+
@on_login_failure.try(:call)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def configured?
|
83
|
+
username.present? && password.present?
|
84
|
+
end
|
85
|
+
|
86
|
+
def soap_enterprise_namespace
|
87
|
+
'urn:enterprise.soap.sforce.com'
|
88
|
+
end
|
89
|
+
|
90
|
+
def server_url
|
91
|
+
"https://#{server_instance}.salesforce.com/services/data/v#{api_version}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def server_host
|
95
|
+
"https://#{server_instance}.salesforce.com"
|
96
|
+
end
|
97
|
+
|
98
|
+
def async_url
|
99
|
+
"https://#{server_instance}.salesforce.com/services/async/#{api_version}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def login_url
|
103
|
+
login_url_base + api_version
|
104
|
+
end
|
105
|
+
|
106
|
+
def login_url_base
|
107
|
+
use_sandbox? ? 'https://test.salesforce.com/services/Soap/c/' : 'https://login.salesforce.com/services/Soap/c/'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|