salesforce_bulk_api 0.0.6 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/salesforce_bulk_api.png)](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
|