salesforce_bulk_api 0.0.6 → 0.0.8
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/.gitignore +3 -0
- data/.rspec +2 -0
- data/README.md +91 -0
- data/Rakefile +3 -1
- data/example_auth_credentials.yml +5 -0
- data/lib/salesforce_bulk_api/connection.rb +25 -5
- data/lib/salesforce_bulk_api/job.rb +138 -69
- data/lib/salesforce_bulk_api/version.rb +1 -1
- data/lib/salesforce_bulk_api.rb +22 -35
- data/salesforce_bulk_api.gemspec +8 -6
- data/spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb +145 -0
- data/spec/spec_helper.rb +16 -0
- metadata +38 -18
- data/README.rdoc +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e63ba9f2d9be3dfa89bf6d896ee94fe847bc1b57
|
4
|
+
data.tar.gz: e054528cb047b09383e98fe2d54b1e3ad628bb0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d66df90281412848aba6eed1a5a5476d7883d1e774f28b21dcd2d3a3952027a9e527a474ec2cd702e5b1018c678a25892572206524d1cfbcbd064e7a7577495
|
7
|
+
data.tar.gz: 0326e9e4f4389d5052ce92aa04ff2daf65c26df997dc31d8b353f3cca60f1eb5634da45d126fb2db2d3221873353cd78df6ca8bab55d43f14199dd0d0de71c33
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Salesforce-Bulk-Api
|
2
|
+
[](http://badge.fury.io/rb/salesforce_bulk_api)
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Salesforce bulk API is a simple ruby gem for connecting to and using the Salesforce Bulk API. It is actually a re-written code from [salesforce_bulk](https://github.com/jorgevaldivia/salesforce_bulk).Written to suit many more other features as well.
|
6
|
+
|
7
|
+
## How to use
|
8
|
+
|
9
|
+
Using this gem is simple and straight forward.
|
10
|
+
|
11
|
+
To initialize:
|
12
|
+
|
13
|
+
`sudo gem install salesforce_bulk_api`
|
14
|
+
|
15
|
+
or add
|
16
|
+
|
17
|
+
`gem salesforce_bulk_api`
|
18
|
+
|
19
|
+
in your Gemfile
|
20
|
+
|
21
|
+
There are two ways to authenticate with SalesForce to use the Bulk API: databasedotcom & restforce.
|
22
|
+
Please check out the entire documentation of the gem you decide to use to learn the various ways of authentication.
|
23
|
+
|
24
|
+
[Databasedotcom](https://github.com/heroku/databasedotcom)
|
25
|
+
[Restforce](https://github.com/ejholmes/restforce)
|
26
|
+
|
27
|
+
|
28
|
+
You can use username password combo, OmniAuth, Oauth2
|
29
|
+
You can use as many records possible in the Array. Governor limits are taken care of inside the gem.
|
30
|
+
|
31
|
+
|
32
|
+
require 'salesforce_bulk_api'
|
33
|
+
client = Databasedotcom::Client.new :client_id => SFDC_APP_CONFIG["client_id"], :client_secret => SFDC_APP_CONFIG["client_secret"] #client_id and client_secret respectively
|
34
|
+
client.authenticate :token => "my-oauth-token", :instance_url => "http://na1.salesforce.com" #=> "my-oauth-token"
|
35
|
+
|
36
|
+
salesforce = SalesforceBulkApi::Api.new(client)
|
37
|
+
|
38
|
+
OR
|
39
|
+
|
40
|
+
require 'salesforce_bulk_api'
|
41
|
+
client = Restforce.new(
|
42
|
+
username: SFDC_APP_CONFIG['SFDC_USERNAME'],
|
43
|
+
password: SFDC_APP_CONFIG['SFDC_PASSWORD'],
|
44
|
+
security_token: SFDC_APP_CONFIG['SFDC_SECURITY_TOKEN'],
|
45
|
+
client_id: SFDC_APP_CONFIG['SFDC_CLIENT_ID'],
|
46
|
+
client_secret: SFDC_APP_CONFIG['SFDC_CLIENT_SECRET'].to_i,
|
47
|
+
host: SFDC_APP_CONFIG['SFDC_HOST']
|
48
|
+
)
|
49
|
+
|
50
|
+
salesforce = SalesforceBulkApi::Api.new(client)
|
51
|
+
|
52
|
+
|
53
|
+
Sample operations:
|
54
|
+
|
55
|
+
# Insert/Create
|
56
|
+
# Add as many fields per record as needed.
|
57
|
+
new_account = Hash["name" => "Test Account", "type" => "Other"]
|
58
|
+
records_to_insert = Array.new
|
59
|
+
# You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
|
60
|
+
records_to_insert.push(new_account)
|
61
|
+
result = salesforce.create("Account", records_to_insert)
|
62
|
+
puts "result is: #{result.inspect}"
|
63
|
+
|
64
|
+
# Update
|
65
|
+
updated_account = Hash["name" => "Test Account -- Updated", id => "a00A0001009zA2m"] # Nearly identical to an insert, but we need to pass the salesforce id.
|
66
|
+
records_to_update = Array.new
|
67
|
+
records_to_update.push(updated_account)
|
68
|
+
salesforce.update("Account", records_to_update)
|
69
|
+
|
70
|
+
# Upsert
|
71
|
+
upserted_account = Hash["name" => "Test Account -- Upserted", "External_Field_Name" => "123456"] # Fields to be updated. External field must be included
|
72
|
+
records_to_upsert = Array.new
|
73
|
+
records_to_upsert.push(upserted_account)
|
74
|
+
salesforce.upsert("Account", records_to_upsert, "External_Field_Name") # Note that upsert accepts an extra parameter for the external field name
|
75
|
+
|
76
|
+
# Delete
|
77
|
+
deleted_account = Hash["id" => "a00A0001009zA2m"] # We only specify the id of the records to delete
|
78
|
+
records_to_delete = Array.new
|
79
|
+
records_to_delete.push(deleted_account)
|
80
|
+
salesforce.delete("Account", records_to_delete)
|
81
|
+
|
82
|
+
# Query
|
83
|
+
res = salesforce.query("Account", "select id, name, createddate from Account limit 3") # We just need to pass the sobject name and the query string
|
84
|
+
|
85
|
+
## Installation
|
86
|
+
|
87
|
+
sudo gem install salesforce_bulk_api
|
88
|
+
|
89
|
+
## Contribute
|
90
|
+
|
91
|
+
Feel to fork and send Pull request
|
data/Rakefile
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module SalesforceBulkApi
|
2
|
+
require 'timeout'
|
2
3
|
|
3
4
|
class Connection
|
4
5
|
|
@@ -22,12 +23,17 @@ module SalesforceBulkApi
|
|
22
23
|
#private
|
23
24
|
|
24
25
|
def login()
|
25
|
-
@
|
26
|
-
|
26
|
+
client_type = @client.class.to_s
|
27
|
+
case client_type
|
28
|
+
when "Restforce::Data::Client"
|
29
|
+
@session_id=@client.options[:oauth_token]
|
30
|
+
@server_url=@client.options[:instance_url]
|
31
|
+
else
|
32
|
+
@session_id=@client.oauth_token
|
33
|
+
@server_url=@client.instance_url
|
34
|
+
end
|
27
35
|
@instance = parse_instance()
|
28
|
-
puts @instance
|
29
36
|
@@INSTANCE_HOST = "#{@instance}.salesforce.com"
|
30
|
-
puts @@INSTANCE_HOST
|
31
37
|
end
|
32
38
|
|
33
39
|
def post_xml(host, path, xml, headers)
|
@@ -36,7 +42,19 @@ module SalesforceBulkApi
|
|
36
42
|
headers['X-SFDC-Session'] = @session_id;
|
37
43
|
path = "#{@@PATH_PREFIX}#{path}"
|
38
44
|
end
|
39
|
-
|
45
|
+
i = 0
|
46
|
+
begin
|
47
|
+
https(host).post(path, xml, headers).body
|
48
|
+
rescue
|
49
|
+
i += 1
|
50
|
+
if i < 3
|
51
|
+
puts "Request fail #{i}: Retrying #{path}"
|
52
|
+
retry
|
53
|
+
else
|
54
|
+
puts "FATAL: Request to #{path} failed three times."
|
55
|
+
raise
|
56
|
+
end
|
57
|
+
end
|
40
58
|
end
|
41
59
|
|
42
60
|
def get_request(host, path, headers)
|
@@ -57,6 +75,8 @@ module SalesforceBulkApi
|
|
57
75
|
|
58
76
|
def parse_instance()
|
59
77
|
@instance=@server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}/).to_s.gsub("https://","")
|
78
|
+
@instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.blank?
|
79
|
+
return @instance
|
60
80
|
end
|
61
81
|
|
62
82
|
end
|
@@ -3,22 +3,25 @@ module SalesforceBulkApi
|
|
3
3
|
class Job
|
4
4
|
|
5
5
|
def initialize(operation, sobject, records, external_field, connection)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
@operation = operation
|
7
|
+
@sobject = sobject
|
8
|
+
@external_field = external_field
|
9
|
+
@records = records
|
10
|
+
@connection = connection
|
11
|
+
@batch_ids = []
|
12
|
+
@XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>'
|
14
13
|
end
|
15
14
|
|
16
|
-
def create_job()
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
def create_job(batch_size, send_nulls, no_null_list)
|
16
|
+
@batch_size = batch_size
|
17
|
+
@send_nulls = send_nulls
|
18
|
+
@no_null_list = no_null_list
|
19
|
+
|
20
|
+
xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
|
21
|
+
xml += "<operation>#{@operation}</operation>"
|
22
|
+
xml += "<object>#{@sobject}</object>"
|
23
|
+
if !@external_field.nil? # This only happens on upsert
|
24
|
+
xml += "<externalIdFieldName>#{@external_field}</externalIdFieldName>"
|
22
25
|
end
|
23
26
|
xml += "<contentType>XML</contentType>"
|
24
27
|
xml += "</jobInfo>"
|
@@ -26,103 +29,169 @@ module SalesforceBulkApi
|
|
26
29
|
path = "job"
|
27
30
|
headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
|
28
31
|
|
29
|
-
response =
|
32
|
+
response = @connection.post_xml(nil, path, xml, headers)
|
30
33
|
response_parsed = XmlSimple.xml_in(response)
|
31
|
-
|
32
|
-
@@job_id = response_parsed['id'][0]
|
34
|
+
@job_id = response_parsed['id'][0]
|
33
35
|
end
|
34
36
|
|
35
37
|
def close_job()
|
36
|
-
xml = "#{
|
38
|
+
xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
|
37
39
|
xml += "<state>Closed</state>"
|
38
40
|
xml += "</jobInfo>"
|
39
41
|
|
40
|
-
path = "job/#{
|
42
|
+
path = "job/#{@job_id}"
|
41
43
|
headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
|
42
44
|
|
43
|
-
response =
|
44
|
-
|
45
|
-
|
46
|
-
#job_id = response_parsed['id'][0]
|
45
|
+
response = @connection.post_xml(nil, path, xml, headers)
|
46
|
+
XmlSimple.xml_in(response)
|
47
47
|
end
|
48
48
|
|
49
49
|
def add_query
|
50
|
-
path = "job/#{
|
50
|
+
path = "job/#{@job_id}/batch/"
|
51
51
|
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
52
52
|
|
53
|
-
response =
|
53
|
+
response = @connection.post_xml(nil, path, @records, headers)
|
54
54
|
response_parsed = XmlSimple.xml_in(response)
|
55
55
|
|
56
|
-
|
56
|
+
@batch_ids << response_parsed['id'][0]
|
57
57
|
end
|
58
58
|
|
59
|
-
def
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
59
|
+
def add_batches
|
60
|
+
raise 'Records must be an array of hashes.' unless @records.is_a? Array
|
61
|
+
keys = @records.reduce({}) {|h,pairs| pairs.each {|k,v| (h[k] ||= []) << v}; h}.keys
|
62
|
+
|
63
|
+
@records_dup = @records.clone
|
64
|
+
|
65
|
+
super_records = []
|
66
|
+
(@records_dup.size/@batch_size).to_i.times do
|
67
|
+
super_records << @records_dup.pop(@batch_size)
|
68
|
+
end
|
69
|
+
super_records << @records_dup unless @records_dup.empty?
|
70
|
+
|
71
|
+
super_records.each do |batch|
|
72
|
+
@batch_ids << add_batch(keys, batch)
|
66
73
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_batch(keys, batch)
|
77
|
+
xml = "#{@XML_HEADER}<sObjects xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
|
78
|
+
batch.each do |r|
|
79
|
+
xml += create_sobject(keys, r)
|
80
|
+
end
|
81
|
+
xml += '</sObjects>'
|
82
|
+
path = "job/#{@job_id}/batch/"
|
83
|
+
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
84
|
+
response = @connection.post_xml(nil, path, xml, headers)
|
85
|
+
response_parsed = XmlSimple.xml_in(response)
|
86
|
+
response_parsed['id'][0] if response_parsed['id']
|
87
|
+
end
|
88
|
+
|
89
|
+
def create_sobject(keys, r)
|
90
|
+
sobject_xml = '<sObject>'
|
91
|
+
keys.each do |k|
|
92
|
+
if !r[k].to_s.empty?
|
93
|
+
sobject_xml += "<#{k}>"
|
94
|
+
if r[k].respond_to?(:encode)
|
95
|
+
sobject_xml += r[k].encode(:xml => :text)
|
96
|
+
else
|
97
|
+
sobject_xml += r[k].to_s
|
75
98
|
end
|
76
|
-
|
99
|
+
sobject_xml += "</#{k}>"
|
100
|
+
elsif @send_nulls && !@no_null_list.include?(k)
|
101
|
+
sobject_xml += "<#{k} xsi:nil=\"true\"/>"
|
77
102
|
end
|
78
|
-
|
79
|
-
|
103
|
+
end
|
104
|
+
sobject_xml += '</sObject>'
|
105
|
+
sobject_xml
|
106
|
+
end
|
80
107
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
108
|
+
def check_job_status
|
109
|
+
path = "job/#{@job_id}"
|
110
|
+
headers = Hash.new
|
111
|
+
response = @connection.get_request(nil, path, headers)
|
85
112
|
|
86
|
-
|
87
|
-
|
113
|
+
begin
|
114
|
+
response_parsed = XmlSimple.xml_in(response) if response
|
115
|
+
response_parsed
|
116
|
+
rescue StandardError => e
|
117
|
+
puts "Error parsing XML response for #{@job_id}"
|
118
|
+
puts e
|
119
|
+
puts e.backtrace
|
88
120
|
end
|
89
121
|
end
|
90
122
|
|
91
|
-
def check_batch_status()
|
92
|
-
path = "job/#{
|
123
|
+
def check_batch_status(batch_id)
|
124
|
+
path = "job/#{@job_id}/batch/#{batch_id}"
|
93
125
|
headers = Hash.new
|
94
126
|
|
95
|
-
response =
|
96
|
-
response_parsed = XmlSimple.xml_in(response)
|
127
|
+
response = @connection.get_request(nil, path, headers)
|
97
128
|
|
98
|
-
# puts response_parsed
|
99
129
|
begin
|
100
|
-
|
130
|
+
response_parsed = XmlSimple.xml_in(response) if response
|
101
131
|
response_parsed
|
102
|
-
rescue
|
103
|
-
|
104
|
-
|
132
|
+
rescue StandardError => e
|
133
|
+
puts "Error parsing XML response for #{@job_id}, batch #{batch_id}"
|
134
|
+
puts e
|
135
|
+
puts e.backtrace
|
105
136
|
end
|
106
137
|
end
|
107
138
|
|
108
|
-
def
|
109
|
-
|
139
|
+
def get_job_result(return_result, timeout)
|
140
|
+
# timeout is in seconds
|
141
|
+
begin
|
142
|
+
state = []
|
143
|
+
Timeout::timeout(timeout, SalesforceBulkApi::JobTimeout) do
|
144
|
+
while true
|
145
|
+
if self.check_job_status['state'][0] == 'Closed'
|
146
|
+
@batch_ids.each do |batch_id|
|
147
|
+
batch_state = self.check_batch_status(batch_id)
|
148
|
+
if batch_state['state'][0] != "Queued" && batch_state['state'][0] != "InProgress"
|
149
|
+
state << (batch_state)
|
150
|
+
@batch_ids.delete(batch_id)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
break if @batch_ids.empty?
|
154
|
+
else
|
155
|
+
break
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
rescue SalesforceBulkApi::JobTimeout => e
|
160
|
+
puts 'Timeout waiting for Salesforce to process job batches #{@batch_ids} of job #{@job_id}.'
|
161
|
+
puts e
|
162
|
+
raise
|
163
|
+
end
|
164
|
+
|
165
|
+
state.each_with_index do |batch_state, i|
|
166
|
+
if batch_state['state'][0] == 'Completed' && return_result == true
|
167
|
+
state[i].merge!({'response' => self.get_batch_result(batch_state['id'][0])})
|
168
|
+
end
|
169
|
+
end
|
170
|
+
state
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_batch_result(batch_id)
|
174
|
+
path = "job/#{@job_id}/batch/#{batch_id}/result"
|
110
175
|
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
111
176
|
|
112
|
-
response =
|
177
|
+
response = @connection.get_request(nil, path, headers)
|
178
|
+
response_parsed = XmlSimple.xml_in(response)
|
179
|
+
results = response_parsed['result'] unless @operation == 'query'
|
113
180
|
|
114
|
-
if(
|
115
|
-
response_parsed = XmlSimple.xml_in(response)
|
181
|
+
if(@operation == 'query') # The query op requires us to do another request to get the results
|
116
182
|
result_id = response_parsed["result"][0]
|
117
|
-
|
118
|
-
path = "job/#{@@job_id}/batch/#{@@batch_id}/result/#{result_id}"
|
183
|
+
path = "job/#{@job_id}/batch/#{batch_id}/result/#{result_id}"
|
119
184
|
headers = Hash.new
|
120
185
|
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
121
|
-
response =
|
186
|
+
response = @connection.get_request(nil, path, headers)
|
187
|
+
response_parsed = XmlSimple.xml_in(response)
|
188
|
+
results = response_parsed['records']
|
122
189
|
end
|
123
|
-
|
124
|
-
|
190
|
+
results
|
125
191
|
end
|
126
192
|
|
127
193
|
end
|
194
|
+
|
195
|
+
class JobTimeout < StandardError
|
196
|
+
end
|
128
197
|
end
|
data/lib/salesforce_bulk_api.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.require()
|
1
4
|
require "salesforce_bulk_api/version"
|
2
5
|
require 'net/https'
|
3
|
-
require 'rubygems'
|
4
6
|
require 'xmlsimple'
|
5
7
|
require 'csv'
|
6
|
-
require "salesforce_bulk_api/version"
|
7
8
|
require 'salesforce_bulk_api/job'
|
8
9
|
require 'salesforce_bulk_api/connection'
|
9
10
|
|
10
11
|
module SalesforceBulkApi
|
11
|
-
|
12
|
+
|
12
13
|
class Api
|
13
14
|
|
14
15
|
@@SALESFORCE_API_VERSION = '23.0'
|
@@ -17,50 +18,36 @@ module SalesforceBulkApi
|
|
17
18
|
@connection = SalesforceBulkApi::Connection.new(@@SALESFORCE_API_VERSION,client)
|
18
19
|
end
|
19
20
|
|
20
|
-
def upsert(sobject, records, external_field)
|
21
|
-
self.do_operation('upsert', sobject, records, external_field)
|
21
|
+
def upsert(sobject, records, external_field, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500)
|
22
|
+
self.do_operation('upsert', sobject, records, external_field, get_response, timeout, batch_size, send_nulls, no_null_list)
|
22
23
|
end
|
23
24
|
|
24
|
-
def update(sobject, records)
|
25
|
-
self.do_operation('update', sobject, records, nil)
|
25
|
+
def update(sobject, records, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500)
|
26
|
+
self.do_operation('update', sobject, records, nil, get_response, timeout, batch_size, send_nulls, no_null_list)
|
26
27
|
end
|
27
28
|
|
28
|
-
def create(sobject, records)
|
29
|
-
self.do_operation('insert', sobject, records, nil)
|
29
|
+
def create(sobject, records, get_response = false, send_nulls = false, batch_size = 10000, timeout = 1500)
|
30
|
+
self.do_operation('insert', sobject, records, nil, get_response, timeout, batch_size, send_nulls)
|
30
31
|
end
|
31
32
|
|
32
|
-
def delete(sobject, records)
|
33
|
-
self.do_operation('delete', sobject, records, nil)
|
33
|
+
def delete(sobject, records, get_response = false, batch_size = 10000, timeout = 1500)
|
34
|
+
self.do_operation('delete', sobject, records, nil, get_response, timeout, batch_size)
|
34
35
|
end
|
35
36
|
|
36
|
-
def query(sobject, query)
|
37
|
-
self.do_operation('query', sobject, query, nil)
|
37
|
+
def query(sobject, query, batch_size = 10000, timeout = 1500)
|
38
|
+
self.do_operation('query', sobject, query, nil, true, timeout, batch_size)
|
38
39
|
end
|
39
40
|
|
40
41
|
#private
|
41
42
|
|
42
|
-
def do_operation(operation, sobject, records, external_field)
|
43
|
+
def do_operation(operation, sobject, records, external_field, get_response, timeout, batch_size, send_nulls = false, no_null_list = [])
|
43
44
|
job = SalesforceBulkApi::Job.new(operation, sobject, records, external_field, @connection)
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
job.
|
49
|
-
|
50
|
-
while true
|
51
|
-
state = job.check_batch_status()
|
52
|
-
if state['state'][0] != "Queued" && state['state'][0] != "InProgress"
|
53
|
-
break
|
54
|
-
end
|
55
|
-
sleep(2) # wait x seconds and check again
|
56
|
-
end
|
57
|
-
|
58
|
-
if state['state'][0] == 'Completed'
|
59
|
-
job.get_batch_result()
|
60
|
-
return state
|
61
|
-
else
|
62
|
-
return "error"
|
63
|
-
end
|
46
|
+
job.create_job(batch_size, send_nulls, no_null_list)
|
47
|
+
operation == "query" ? job.add_query() : job.add_batches()
|
48
|
+
response = job.close_job
|
49
|
+
response.merge!({'batches' => job.get_job_result(get_response, timeout)}) if get_response == true
|
50
|
+
response
|
64
51
|
end
|
65
|
-
end
|
66
|
-
end
|
52
|
+
end
|
53
|
+
end
|
data/salesforce_bulk_api.gemspec
CHANGED
@@ -7,21 +7,23 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = SalesforceBulkApi::VERSION
|
8
8
|
s.authors = ["Yatish Mehta"]
|
9
9
|
s.email = ["yatishmehta27@gmail.com"]
|
10
|
+
|
10
11
|
s.homepage = "https://github.com/yatishmehta27/salesforce_bulk_api"
|
11
|
-
s.summary = %q{It uses the bulk api of salesforce to communicate with
|
12
|
+
s.summary = %q{It uses the bulk api of salesforce to communicate with Salesforce CRM}
|
12
13
|
s.description = %q{Salesforce Bulk API with governor limits taken care of}
|
13
14
|
|
14
15
|
s.rubyforge_project = "salesforce_bulk_api"
|
15
|
-
|
16
|
-
s.add_dependency(%q<databasedotcom>, [">= 0"])
|
16
|
+
|
17
17
|
s.add_dependency(%q<json>, [">= 0"])
|
18
18
|
s.add_dependency(%q<xml-simple>, [">= 0"])
|
19
|
+
|
20
|
+
s.add_development_dependency "rspec"
|
21
|
+
s.add_development_dependency("webmock", ["~> 1.13"])
|
22
|
+
s.add_development_dependency("vcr", ['~> 2.5'])
|
23
|
+
|
19
24
|
s.files = `git ls-files`.split("\n")
|
20
25
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
21
26
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
27
|
s.require_paths = ["lib"]
|
23
28
|
|
24
|
-
# specify any dependencies here; for example:
|
25
|
-
# s.add_development_dependency "rspec"
|
26
|
-
# s.add_runtime_dependency "rest-client"
|
27
29
|
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yaml'
|
3
|
+
require 'databasedotcom'
|
4
|
+
|
5
|
+
describe SalesforceBulkApi do
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
auth_hash = YAML.load(File.read('auth_credentials.yml'))
|
9
|
+
@sf_client = Databasedotcom::Client.new(:client_id => auth_hash['salesforce']['client_id'],
|
10
|
+
:client_secret => auth_hash['salesforce']['client_secret'])
|
11
|
+
@sf_client.authenticate(:username => auth_hash['salesforce']['user'], :password => auth_hash['salesforce']['passwordandtoken'])
|
12
|
+
@api = SalesforceBulkApi::Api.new(@sf_client)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'upsert' do
|
16
|
+
|
17
|
+
context 'when not passed get_result' do
|
18
|
+
it "doesn't return the batches array" do
|
19
|
+
res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}], 'Id')
|
20
|
+
res['batches'].should be_nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when passed get_result = true' do
|
25
|
+
it 'returns the batches array' do
|
26
|
+
res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}], 'Id', true)
|
27
|
+
res['batches'][0]['response'].is_a? Array
|
28
|
+
res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when passed send_nulls = true' do
|
33
|
+
it 'sets the nil and empty attributes to NULL' do
|
34
|
+
@api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'abc123', :Other_Phone__c => '5678', :Gold_Star__c => true}], true)
|
35
|
+
res = @api.query('Account', "SELECT Website, Other_Phone__c From Account WHERE Id = '0013000000ymMBh'")
|
36
|
+
res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
|
37
|
+
res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq '5678'
|
38
|
+
res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => '', :Other_Phone__c => nil, :Gold_Star__c => false, :CRM_Last_Modified__c => nil}], 'Id', true, true)
|
39
|
+
res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
|
40
|
+
res = @api.query('Account', "SELECT Website, Other_Phone__c, Gold_Star__c, CRM_Last_Modified__c From Account WHERE Id = '0013000000ymMBh'")
|
41
|
+
res['batches'][0]['response'][0]['Website'][0].should eq({"xsi:nil" => "true"})
|
42
|
+
res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq({"xsi:nil" => "true"})
|
43
|
+
res['batches'][0]['response'][0]['Gold_Star__c'][0].should eq('false')
|
44
|
+
res['batches'][0]['response'][0]['CRM_Last_Modified__c'][0].should eq({"xsi:nil" => "true"})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when passed send_nulls = true and an array of fields not to null' do
|
49
|
+
it 'sets the nil and empty attributes to NULL, except for those included in the list of fields to ignore' do
|
50
|
+
@api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'abc123', :Other_Phone__c => '5678', :Gold_Star__c => true}], true)
|
51
|
+
res = @api.query('Account', "SELECT Website, Other_Phone__c From Account WHERE Id = '0013000000ymMBh'")
|
52
|
+
res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
|
53
|
+
res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq '5678'
|
54
|
+
res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => '', :Other_Phone__c => nil, :Gold_Star__c => false, :CRM_Last_Modified__c => nil}], 'Id', true, true, [:Website, :Other_Phone__c])
|
55
|
+
res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
|
56
|
+
res = @api.query('Account', "SELECT Website, Other_Phone__c, Gold_Star__c, CRM_Last_Modified__c From Account WHERE Id = '0013000000ymMBh'")
|
57
|
+
res['batches'][0]['response'][0]['Website'][0].should eq('abc123')
|
58
|
+
res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq('5678')
|
59
|
+
res['batches'][0]['response'][0]['Gold_Star__c'][0].should eq('false')
|
60
|
+
res['batches'][0]['response'][0]['CRM_Last_Modified__c'][0].should eq({"xsi:nil" => "true"})
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'update' do
|
67
|
+
context 'when there is not an error' do
|
68
|
+
context 'when not passed get_result' do
|
69
|
+
it "doesnt return the batches array" do
|
70
|
+
res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}])
|
71
|
+
res['batches'].should be_nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when passed get_result = true' do
|
76
|
+
it 'returns the batches array' do
|
77
|
+
res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}], true)
|
78
|
+
res['batches'][0]['response'].is_a? Array
|
79
|
+
res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'when there is an error' do
|
85
|
+
context 'when not passed get_result' do
|
86
|
+
it "doesn't return the results array" do
|
87
|
+
res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'},{:Id => 'abc123', :Website => 'www.test.com'}])
|
88
|
+
res['batches'].should be_nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when passed get_result = true with batches' do
|
93
|
+
it 'returns the results array' do
|
94
|
+
res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}, {:Id => '0013000000ymMBh', :Website => 'www.test.com'}, {:Id => '0013000000ymMBh', :Website => 'www.test.com'}, {:Id => 'abc123', :Website => 'www.test.com'}], true, false, [], 2)
|
95
|
+
res['batches'][0]['response'].should eq([{"id"=>["0013000000ymMBhAAM"], "success"=>["true"], "created"=>["false"]}, {"errors"=>[{"fields"=>["Id"], "message"=>["Account ID: id value of incorrect type: abc123"], "statusCode"=>["MALFORMED_ID"]}], "success"=>["false"], "created"=>["false"]}])
|
96
|
+
res['batches'][1]['response'].should eq([{"id"=>["0013000000ymMBhAAM"], "success"=>["true"], "created"=>["false"]},{"id"=>["0013000000ymMBhAAM"], "success"=>["true"], "created"=>["false"]}])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'create' do
|
104
|
+
pending
|
105
|
+
end
|
106
|
+
|
107
|
+
describe 'delete' do
|
108
|
+
pending
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'query' do
|
112
|
+
|
113
|
+
context 'when there are results' do
|
114
|
+
it 'returns the query results' do
|
115
|
+
res = @api.query('Account', "SELECT id, Name From Account WHERE Name LIKE 'Test%'")
|
116
|
+
res['batches'][0]['response'].length.should > 1
|
117
|
+
res['batches'][0]['response'][0]['Id'].should_not be_nil
|
118
|
+
end
|
119
|
+
context 'and there are multiple batches' do
|
120
|
+
it 'returns the query results in a merged hash' do
|
121
|
+
pending 'need dev to create > 10k records in dev organization'
|
122
|
+
res = @api.query('Account', "SELECT id, Name From Account WHERE Name LIKE 'Test%'")
|
123
|
+
res['batches'][0]['response'].length.should > 1
|
124
|
+
res['batches'][0]['response'][0]['Id'].should_not be_nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'when there are no results' do
|
130
|
+
it 'returns nil' do
|
131
|
+
res = @api.query('Account', "SELECT id From Account WHERE Name = 'ABC'")
|
132
|
+
res['batches'][0]['response'].should eq nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'when there is an error' do
|
137
|
+
it 'returns nil' do
|
138
|
+
res = @api.query('Account', "SELECT id From Account WHERE Name = ''ABC'")
|
139
|
+
res['batches'][0]['response'].should eq nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
#require 'webmock/rspec'
|
4
|
+
#require 'vcr'
|
5
|
+
require 'salesforce_bulk_api'
|
6
|
+
|
7
|
+
RSpec.configure do |c|
|
8
|
+
c.filter_run :focus => true
|
9
|
+
c.run_all_when_everything_filtered = true
|
10
|
+
end
|
11
|
+
|
12
|
+
# enable this and record the test requests using a SF developer org.
|
13
|
+
# VCR.configure do |c|
|
14
|
+
# c.cassette_library_dir = 'spec/cassettes'
|
15
|
+
# c.hook_into :webmock
|
16
|
+
# end
|
metadata
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: salesforce_bulk_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yatish Mehta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-01-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: json
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - '>='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0
|
19
|
+
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '>='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0
|
26
|
+
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: xml-simple
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - '>='
|
@@ -39,13 +39,13 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - '>='
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
|
-
type: :
|
48
|
+
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
@@ -53,19 +53,33 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: webmock
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ~>
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
62
|
-
type: :
|
61
|
+
version: '1.13'
|
62
|
+
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ~>
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '1.13'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: vcr
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.5'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.5'
|
69
83
|
description: Salesforce Bulk API with governor limits taken care of
|
70
84
|
email:
|
71
85
|
- yatishmehta27@gmail.com
|
@@ -74,14 +88,18 @@ extensions: []
|
|
74
88
|
extra_rdoc_files: []
|
75
89
|
files:
|
76
90
|
- .gitignore
|
91
|
+
- .rspec
|
77
92
|
- Gemfile
|
78
|
-
- README.
|
93
|
+
- README.md
|
79
94
|
- Rakefile
|
95
|
+
- example_auth_credentials.yml
|
80
96
|
- lib/salesforce_bulk_api.rb
|
81
97
|
- lib/salesforce_bulk_api/connection.rb
|
82
98
|
- lib/salesforce_bulk_api/job.rb
|
83
99
|
- lib/salesforce_bulk_api/version.rb
|
84
100
|
- salesforce_bulk_api.gemspec
|
101
|
+
- spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb
|
102
|
+
- spec/spec_helper.rb
|
85
103
|
homepage: https://github.com/yatishmehta27/salesforce_bulk_api
|
86
104
|
licenses: []
|
87
105
|
metadata: {}
|
@@ -101,8 +119,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
119
|
version: '0'
|
102
120
|
requirements: []
|
103
121
|
rubyforge_project: salesforce_bulk_api
|
104
|
-
rubygems_version: 2.
|
122
|
+
rubygems_version: 2.1.11
|
105
123
|
signing_key:
|
106
124
|
specification_version: 4
|
107
|
-
summary: It uses the bulk api of salesforce to communicate with
|
108
|
-
test_files:
|
125
|
+
summary: It uses the bulk api of salesforce to communicate with Salesforce CRM
|
126
|
+
test_files:
|
127
|
+
- spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb
|
128
|
+
- spec/spec_helper.rb
|
data/README.rdoc
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
= Salesforce-Bulk-Api
|
2
|
-
|
3
|
-
==Overview
|
4
|
-
|
5
|
-
Salesforce bulk Api is a simple ruby gem for connecting to and using the Salesforce Bulk API. It is actually a re-written code from salesforce_bulk[https://github.com/jorgevaldivia/salesforce_bulk].Written to suit many more other features as well.
|
6
|
-
|
7
|
-
==How to use
|
8
|
-
|
9
|
-
Using this gem is simple and straight forward.
|
10
|
-
|
11
|
-
To initialize:
|
12
|
-
sudo gem install salesforce_bulk_api
|
13
|
-
or add
|
14
|
-
gem salesforce_bulk_api
|
15
|
-
in your Gemfile
|
16
|
-
|
17
|
-
|
18
|
-
Please do check the entire documentation of the databasedotcom gem and it various ways of authentication
|
19
|
-
Databasedotcom[https://github.com/heroku/databasedotcom]
|
20
|
-
|
21
|
-
You can use username password combo, OmniAuth, Oauth2
|
22
|
-
You can as many records possible in the Array. Governor limits are taken care of inside the gem.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
require 'salesforce_bulk_api'
|
27
|
-
client = Databasedotcom::Client.new :client_id => $SFDC_APP_CONFIG["client_id"], :client_secret => $SFDC_APP_CONFIG["client_secret"] #client_id and client_secret respectively
|
28
|
-
client.authenticate :token => "my-oauth-token", :instance_url => "http://na1.salesforce.com" #=> "my-oauth-token"
|
29
|
-
salesforce = SalesforceBulkApi::Api.new(client)
|
30
|
-
|
31
|
-
Sample operations:
|
32
|
-
|
33
|
-
# Insert/Create
|
34
|
-
new_account = Hash["name" => "Test Account", "type" => "Other"] # Add as many fields per record as needed.
|
35
|
-
records_to_insert = Array.new
|
36
|
-
records_to_insert.push(new_account) # You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
|
37
|
-
result = salesforce.create("Account", records_to_insert)
|
38
|
-
puts "result is: #{result.inspect}"
|
39
|
-
|
40
|
-
# Update
|
41
|
-
updated_account = Hash["name" => "Test Account -- Updated", id => "a00A0001009zA2m"] # Nearly identical to an insert, but we need to pass the salesforce id.
|
42
|
-
records_to_update = Array.new
|
43
|
-
records_to_update.push(updated_account)
|
44
|
-
salesforce.update("Account", records_to_update)
|
45
|
-
|
46
|
-
# Upsert
|
47
|
-
upserted_account = Hash["name" => "Test Account -- Upserted", "External_Field_Name" => "123456"] # Fields to be updated. External field must be included
|
48
|
-
records_to_upsert = Array.new
|
49
|
-
records_to_upsert.push(upserted_account)
|
50
|
-
salesforce.upsert("Account", records_to_upsert, "External_Field_Name") # Note that upsert accepts an extra parameter for the external field name
|
51
|
-
|
52
|
-
# Delete
|
53
|
-
deleted_account = Hash["id" => "a00A0001009zA2m"] # We only specify the id of the records to delete
|
54
|
-
records_to_delete = Array.new
|
55
|
-
records_to_delete.push(deleted_account)
|
56
|
-
salesforce.delete("Account", records_to_delete)
|
57
|
-
|
58
|
-
# Query
|
59
|
-
res = salesforce.query("Account", "select id, name, createddate from Account limit 3") # We just need to pass the sobject name and the query string
|
60
|
-
|
61
|
-
==Installation
|
62
|
-
sudo gem install salesforce_bulk_api
|
63
|
-
|
64
|
-
== Copyright
|
65
|
-
|
66
|
-
Copyright (c) 2012 Yatish Mehta
|