activeforce 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|