salesforcebulk 1.4.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +33 -15
- data/lib/salesforce_bulk.rb +1 -5
- data/lib/salesforce_bulk/client.rb +56 -59
- data/lib/salesforce_bulk/version.rb +1 -1
- metadata +46 -76
- data/.gitignore +0 -4
- data/.travis.yml +0 -10
- data/Gemfile +0 -3
- data/LICENSE +0 -20
- data/Rakefile +0 -22
- data/salesforcebulk.gemspec +0 -30
- data/test/fixtures/batch_create_request.csv +0 -3
- data/test/fixtures/batch_create_response.xml +0 -13
- data/test/fixtures/batch_info_list_response.xml +0 -27
- data/test/fixtures/batch_info_response.xml +0 -14
- data/test/fixtures/batch_result_list_response.csv +0 -3
- data/test/fixtures/config.yml +0 -5
- data/test/fixtures/invalid_batch_error.xml +0 -5
- data/test/fixtures/invalid_error.xml +0 -5
- data/test/fixtures/invalid_job_error.xml +0 -5
- data/test/fixtures/invalid_session_error.xml +0 -5
- data/test/fixtures/job_abort_request.xml +0 -1
- data/test/fixtures/job_abort_response.xml +0 -25
- data/test/fixtures/job_close_request.xml +0 -1
- data/test/fixtures/job_close_response.xml +0 -25
- data/test/fixtures/job_create_request.xml +0 -1
- data/test/fixtures/job_create_response.xml +0 -25
- data/test/fixtures/job_info_response.xml +0 -25
- data/test/fixtures/login_error.xml +0 -1
- data/test/fixtures/login_request.xml +0 -1
- data/test/fixtures/login_response.xml +0 -39
- data/test/fixtures/query_result_list_response.xml +0 -1
- data/test/fixtures/query_result_response.csv +0 -5
- data/test/lib/test_batch.rb +0 -252
- data/test/lib/test_batch_result.rb +0 -36
- data/test/lib/test_core_extensions.rb +0 -15
- data/test/lib/test_initialization.rb +0 -80
- data/test/lib/test_job.rb +0 -247
- data/test/lib/test_query_result_collection.rb +0 -86
- data/test/test_helper.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b79868012c05ed38e161bcb12bb95eb0382e5b3
|
4
|
+
data.tar.gz: c3a7e13b192b7b2ceafe17a2214935428d9ff3a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c14b55e95b378444129d266c48fafa0421f033f46250105504a0ac8f5b150e73b6873eedffc566d3abca5b1782e1cd469ac320165a0fb6ff7c28b53473bfa0a
|
7
|
+
data.tar.gz: 8ec9210c38ab0ccb04fa341c85ba88cfd8ffe01631babfcf6ef30fed96a06e82634b3ac27bfbc981bc771f6c24b61d3cccf2dc562de3540d21a7b0b742a70ad2
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
## Overview
|
4
4
|
|
5
|
-
SalesforceBulk is an easy to use Ruby gem for connecting to and using the [Salesforce Bulk API](http://www.salesforce.com/us/developer/docs/api_asynch/index.htm). This is a rewrite and separate release of Jorge Valdivia's salesforce_bulk gem (renamed `salesforcebulk`) with full unit tests and full API capability (e.g. adding multiple batches per job). This gem was built on Ruby
|
5
|
+
SalesforceBulk is an easy to use Ruby gem for connecting to and using the [Salesforce Bulk API](http://www.salesforce.com/us/developer/docs/api_asynch/index.htm). This is a rewrite and separate release of Jorge Valdivia's salesforce_bulk gem (renamed `salesforcebulk`) with full unit tests and full API capability (e.g. adding multiple batches per job). This gem was built on Ruby 2.0.0, 2.1.6, and 2.2.2.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -21,6 +21,14 @@ To contribute, fork this repo, create a topic branch, make changes, then send a
|
|
21
21
|
bundle install
|
22
22
|
rake
|
23
23
|
|
24
|
+
To run the test suite on all gemfiles with your current ruby version, use:
|
25
|
+
|
26
|
+
bundle exec rake wwtd:local
|
27
|
+
|
28
|
+
To run the full test suite with different gemfiles and ruby versions, use:
|
29
|
+
|
30
|
+
bundle exec rake wwtd
|
31
|
+
|
24
32
|
## Configuration and Initialization
|
25
33
|
|
26
34
|
### Basic Configuration
|
@@ -28,7 +36,7 @@ To contribute, fork this repo, create a topic branch, make changes, then send a
|
|
28
36
|
When retrieving a password you will also be given a security token. Combine the two into a single value as the API treats this as your real password.
|
29
37
|
|
30
38
|
require 'salesforce_bulk'
|
31
|
-
|
39
|
+
|
32
40
|
client = SalesforceBulk::Client.new(username: 'MyUsername', password: 'MyPasswordWithSecurtyToken')
|
33
41
|
client.authenticate
|
34
42
|
|
@@ -47,7 +55,7 @@ Create a YAML file with the content below. Only `username` and `password` is req
|
|
47
55
|
Then in a Ruby script:
|
48
56
|
|
49
57
|
require 'salesforce_bulk'
|
50
|
-
|
58
|
+
|
51
59
|
client = SalesforceBulk::Client.new("config/salesforce_bulk.yml")
|
52
60
|
client.authenticate
|
53
61
|
|
@@ -59,13 +67,13 @@ An important note about the data in any of the examples below: each hash in a da
|
|
59
67
|
|
60
68
|
data1 = [{:Name__c => 'Test 1'}, {:Name__c => 'Test 2'}]
|
61
69
|
data2 = [{:Name__c => 'Test 3'}, {:Name__c => 'Test 4'}]
|
62
|
-
|
70
|
+
|
63
71
|
job = client.add_job(:insert, :MyObject__c)
|
64
|
-
|
72
|
+
|
65
73
|
# easily add multiple batches to a job
|
66
74
|
batch = client.add_batch(job.id, data1)
|
67
75
|
batch = client.add_batch(job.id, data2)
|
68
|
-
|
76
|
+
|
69
77
|
job = client.close_job(job.id) # or use the abort_job(id) method
|
70
78
|
|
71
79
|
### Adding a Job
|
@@ -90,7 +98,7 @@ For any operation you should be able to specify a concurrency mode. The default
|
|
90
98
|
The Job object has various properties such as status, created time, number of completed and failed batches and various other values.
|
91
99
|
|
92
100
|
job = client.job_info(jobId) # returns a Job object
|
93
|
-
|
101
|
+
|
94
102
|
puts "Job #{job.id} is closed." if job.closed? # other: open?, aborted?
|
95
103
|
|
96
104
|
### Retrieving Info for a single Batch
|
@@ -98,13 +106,13 @@ The Job object has various properties such as status, created time, number of co
|
|
98
106
|
The Batch object has various properties such as status, created time, number of processed and failed records and various other values.
|
99
107
|
|
100
108
|
batch = client.batch_info(jobId, batchId) # returns a Batch object
|
101
|
-
|
109
|
+
|
102
110
|
puts "Batch #{batch.id} is in progress." if batch.in_progress?
|
103
111
|
|
104
112
|
### Retrieving Info for all Batches
|
105
113
|
|
106
114
|
batches = client.batch_info_list(jobId) # returns an Array of Batch objects
|
107
|
-
|
115
|
+
|
108
116
|
batches.each do |batch|
|
109
117
|
puts "Batch #{batch.id} completed." if batch.completed? # other: failed?, in_progress?, queued?
|
110
118
|
end
|
@@ -116,7 +124,7 @@ To verify that a batch completed successfully or failed call the `batch_info` or
|
|
116
124
|
The object returned from the following example only applies to the operations: `delete`, `insert`, `update` and `upsert`. Query results are handled differently.
|
117
125
|
|
118
126
|
results = client.batch_result(jobId, batchId) # returns an Array of BatchResult objects
|
119
|
-
|
127
|
+
|
120
128
|
results.each do |result|
|
121
129
|
puts "Item #{result.id} had an error of: #{result.error}" if result.error?
|
122
130
|
end
|
@@ -129,18 +137,18 @@ Query results are handled differently as its possible that a single batch could
|
|
129
137
|
|
130
138
|
# returns a QueryResultCollection object (an Array)
|
131
139
|
results = client.batch_result(jobId, batchId)
|
132
|
-
|
140
|
+
|
133
141
|
while results.any?
|
134
|
-
|
142
|
+
|
135
143
|
# Assuming query was: SELECT Id, Name, CustomField__c FROM Account
|
136
144
|
results.each do |result|
|
137
145
|
puts result[:Id], result[:Name], result[:CustomField__c]
|
138
146
|
end
|
139
|
-
|
147
|
+
|
140
148
|
puts "Another set is available." if results.next?
|
141
|
-
|
149
|
+
|
142
150
|
results.next
|
143
|
-
|
151
|
+
|
144
152
|
end
|
145
153
|
|
146
154
|
Note: By reviewing the API docs and response format my understanding was that the API would return multiple results sets for a single batch if the query was to large but this does not seem to be the case in my live testing. It seems to be capped at 10000 records (as it when inserting data) but I haven't been able to verify through the documentation. If you know anything about that your input is appreciated. In the meantime the gem was built to support multiple result sets for a query batch but seems that will change which will simplify that method.
|
@@ -153,6 +161,16 @@ Note: By reviewing the API docs and response format my understanding was that th
|
|
153
161
|
|
154
162
|
## Version History
|
155
163
|
|
164
|
+
**2.0.0** (April 25, 2015)
|
165
|
+
|
166
|
+
* Dropped support for Ruby 1.8 and Ruby 1.9
|
167
|
+
* Added support for Ruby 2.0, 2.1 and 2.2
|
168
|
+
* Added support for Rails 4.0, 4.1 an 4.2
|
169
|
+
* Changed test_helper to avoid requiring test_unit (removed in Ruby 2.2)
|
170
|
+
* Replaced Test::Unit::TestCase with ActiveSupport::TestCase
|
171
|
+
* Bumped shoulda and losen dependencies on minitest
|
172
|
+
* All changes in PR's #13, #14, #15, #16 - thanks [@pschambacher](https://github.com/pschambacher)
|
173
|
+
|
156
174
|
**1.4.0** (June 1, 2014)
|
157
175
|
|
158
176
|
* Added state_message to Batch class (#11 - thanks [@bethesque](https://github.com/bethesque))
|
data/lib/salesforce_bulk.rb
CHANGED
@@ -1,45 +1,42 @@
|
|
1
1
|
module SalesforceBulk
|
2
|
-
if RUBY_VERSION < "1.9"
|
3
|
-
CSV = ::FasterCSV
|
4
|
-
end
|
5
2
|
|
6
3
|
# Interface for operating the Salesforce Bulk REST API
|
7
4
|
class Client
|
8
5
|
# The host to use for authentication. Defaults to login.salesforce.com.
|
9
6
|
attr_accessor :login_host
|
10
|
-
|
7
|
+
|
11
8
|
# The instance host to use for API calls. Determined from login response.
|
12
9
|
attr_accessor :instance_host
|
13
|
-
|
10
|
+
|
14
11
|
# The Salesforce password
|
15
12
|
attr_accessor :password
|
16
|
-
|
13
|
+
|
17
14
|
# The Salesforce username
|
18
15
|
attr_accessor :username
|
19
|
-
|
16
|
+
|
20
17
|
# The API version the client is using. Defaults to 24.0.
|
21
18
|
attr_accessor :version
|
22
|
-
|
19
|
+
|
23
20
|
def initialize(options={})
|
24
21
|
if options.is_a?(String)
|
25
22
|
options = YAML.load_file(options)
|
26
23
|
options.symbolize_keys!
|
27
24
|
end
|
28
|
-
|
25
|
+
|
29
26
|
options = {:login_host => 'login.salesforce.com', :version => 24.0}.merge(options)
|
30
|
-
|
27
|
+
|
31
28
|
options.assert_valid_keys(:username, :password, :login_host, :version)
|
32
|
-
|
29
|
+
|
33
30
|
self.username = options[:username]
|
34
31
|
self.password = "#{options[:password]}"
|
35
32
|
self.login_host = options[:login_host]
|
36
33
|
self.version = options[:version]
|
37
|
-
|
34
|
+
|
38
35
|
@api_path_prefix = "/services/async/#{version}/"
|
39
36
|
@valid_operations = [:delete, :insert, :update, :upsert, :query]
|
40
37
|
@valid_concurrency_modes = ['Parallel', 'Serial']
|
41
38
|
end
|
42
|
-
|
39
|
+
|
43
40
|
def authenticate
|
44
41
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
45
42
|
xml += '<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
|
@@ -52,63 +49,63 @@ module SalesforceBulk
|
|
52
49
|
xml += "</n1:login>"
|
53
50
|
xml += "</env:Body>"
|
54
51
|
xml += "</env:Envelope>\n"
|
55
|
-
|
52
|
+
|
56
53
|
response = http_post("/services/Soap/u/#{version}", xml, 'Content-Type' => 'text/xml', 'SOAPAction' => 'login')
|
57
|
-
|
54
|
+
|
58
55
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
59
56
|
result = data['Body']['loginResponse']['result']
|
60
|
-
|
57
|
+
|
61
58
|
@session_id = result['sessionId']
|
62
|
-
|
59
|
+
|
63
60
|
self.instance_host = "#{instance_id(result['serverUrl'])}.salesforce.com"
|
64
61
|
self
|
65
62
|
end
|
66
|
-
|
63
|
+
|
67
64
|
def abort_job(jobId)
|
68
65
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
69
66
|
xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
|
70
67
|
xml += "<state>Aborted</state>"
|
71
68
|
xml += "</jobInfo>"
|
72
|
-
|
69
|
+
|
73
70
|
response = http_post("job/#{jobId}", xml)
|
74
71
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
75
72
|
Job.new_from_xml(data)
|
76
73
|
end
|
77
|
-
|
74
|
+
|
78
75
|
def add_batch(jobId, data)
|
79
76
|
body = data
|
80
|
-
|
77
|
+
|
81
78
|
if data.is_a?(Array)
|
82
79
|
raise ArgumentError, "Data set exceeds 10000 record limit by #{data.length - 10000}" if data.length > 10000
|
83
|
-
|
80
|
+
|
84
81
|
keys = data.first.keys
|
85
82
|
body = keys.to_csv
|
86
|
-
|
83
|
+
|
87
84
|
data.each do |item|
|
88
85
|
item_values = keys.map { |key| item[key] }
|
89
86
|
body += item_values.to_csv
|
90
87
|
end
|
91
88
|
end
|
92
|
-
|
93
|
-
# Despite the content for a query operation batch being plain text we
|
89
|
+
|
90
|
+
# Despite the content for a query operation batch being plain text we
|
94
91
|
# still have to specify CSV content type per API docs.
|
95
92
|
response = http_post("job/#{jobId}/batch", body, "Content-Type" => "text/csv; charset=UTF-8")
|
96
93
|
result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
97
94
|
Batch.new_from_xml(result)
|
98
95
|
end
|
99
|
-
|
96
|
+
|
100
97
|
def add_job(operation, sobject, options={})
|
101
98
|
operation = operation.to_s.downcase.to_sym
|
102
|
-
|
99
|
+
|
103
100
|
raise ArgumentError.new("Invalid operation: #{operation}") unless @valid_operations.include?(operation)
|
104
|
-
|
101
|
+
|
105
102
|
options.assert_valid_keys(:external_id_field_name, :concurrency_mode)
|
106
|
-
|
103
|
+
|
107
104
|
if options[:concurrency_mode]
|
108
105
|
concurrency_mode = options[:concurrency_mode].capitalize
|
109
106
|
raise ArgumentError.new("Invalid concurrency mode: #{concurrency_mode}") unless @valid_concurrency_modes.include?(concurrency_mode)
|
110
107
|
end
|
111
|
-
|
108
|
+
|
112
109
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
113
110
|
xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
|
114
111
|
xml += "<operation>#{operation}</operation>"
|
@@ -117,16 +114,16 @@ module SalesforceBulk
|
|
117
114
|
xml += "<concurrencyMode>#{options[:concurrency_mode]}</concurrencyMode>" if options[:concurrency_mode]
|
118
115
|
xml += "<contentType>CSV</contentType>"
|
119
116
|
xml += "</jobInfo>"
|
120
|
-
|
117
|
+
|
121
118
|
response = http_post("job", xml)
|
122
119
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
123
120
|
job = Job.new_from_xml(data)
|
124
121
|
end
|
125
|
-
|
122
|
+
|
126
123
|
def batch_info_list(jobId)
|
127
124
|
response = http_get("job/#{jobId}/batch")
|
128
125
|
result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
129
|
-
|
126
|
+
|
130
127
|
if result['batchInfo'].is_a?(Array)
|
131
128
|
result['batchInfo'].collect do |info|
|
132
129
|
Batch.new_from_xml(info)
|
@@ -135,73 +132,73 @@ module SalesforceBulk
|
|
135
132
|
[Batch.new_from_xml(result['batchInfo'])]
|
136
133
|
end
|
137
134
|
end
|
138
|
-
|
135
|
+
|
139
136
|
def batch_info(jobId, batchId)
|
140
137
|
response = http_get("job/#{jobId}/batch/#{batchId}")
|
141
138
|
result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
142
139
|
Batch.new_from_xml(result)
|
143
140
|
end
|
144
|
-
|
141
|
+
|
145
142
|
def batch_result(jobId, batchId)
|
146
143
|
response = http_get("job/#{jobId}/batch/#{batchId}/result")
|
147
|
-
|
144
|
+
|
148
145
|
if response.body =~ /<.*?>/m
|
149
146
|
result = XmlSimple.xml_in(response.body)
|
150
|
-
|
147
|
+
|
151
148
|
if result['result'].present?
|
152
149
|
results = query_result(jobId, batchId, result['result'].first)
|
153
|
-
|
150
|
+
|
154
151
|
collection = QueryResultCollection.new(self, jobId, batchId, result['result'].first, result['result'])
|
155
152
|
collection.replace(results)
|
156
153
|
end
|
157
154
|
else
|
158
155
|
result = BatchResultCollection.new(jobId, batchId)
|
159
|
-
|
156
|
+
|
160
157
|
CSV.parse(response.body, :headers => true) do |row|
|
161
158
|
result << BatchResult.new(row[0], row[1].to_b, row[2].to_b, row[3])
|
162
159
|
end
|
163
|
-
|
160
|
+
|
164
161
|
result
|
165
162
|
end
|
166
163
|
end
|
167
|
-
|
164
|
+
|
168
165
|
def query_result(job_id, batch_id, result_id)
|
169
166
|
headers = {"Content-Type" => "text/csv; charset=UTF-8"}
|
170
167
|
response = http_get("job/#{job_id}/batch/#{batch_id}/result/#{result_id}", headers)
|
171
|
-
|
168
|
+
|
172
169
|
lines = response.body.lines.to_a
|
173
170
|
headers = CSV.parse_line(lines.shift).collect { |header| header.to_sym }
|
174
|
-
|
171
|
+
|
175
172
|
result = []
|
176
|
-
|
173
|
+
|
177
174
|
#CSV.parse(lines.join, :headers => headers, :converters => [:all, lambda{|s| s.to_b if s.kind_of? String }]) do |row|
|
178
175
|
CSV.parse(lines.join, :headers => headers) do |row|
|
179
176
|
result << Hash[row.headers.zip(row.fields)]
|
180
177
|
end
|
181
|
-
|
178
|
+
|
182
179
|
result
|
183
180
|
end
|
184
|
-
|
181
|
+
|
185
182
|
def close_job(jobId)
|
186
183
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
187
184
|
xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
|
188
185
|
xml += "<state>Closed</state>"
|
189
186
|
xml += "</jobInfo>"
|
190
|
-
|
187
|
+
|
191
188
|
response = http_post("job/#{jobId}", xml)
|
192
189
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
193
190
|
Job.new_from_xml(data)
|
194
191
|
end
|
195
|
-
|
192
|
+
|
196
193
|
def job_info(jobId)
|
197
194
|
response = http_get("job/#{jobId}")
|
198
195
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
199
196
|
Job.new_from_xml(data)
|
200
197
|
end
|
201
|
-
|
198
|
+
|
202
199
|
def http_post(path, body, headers={})
|
203
200
|
headers = {'Content-Type' => 'application/xml'}.merge(headers)
|
204
|
-
|
201
|
+
|
205
202
|
if @session_id
|
206
203
|
headers['X-SFDC-Session'] = @session_id
|
207
204
|
host = instance_host
|
@@ -209,41 +206,41 @@ module SalesforceBulk
|
|
209
206
|
else
|
210
207
|
host = self.login_host
|
211
208
|
end
|
212
|
-
|
209
|
+
|
213
210
|
response = https_request(host).post(path, body, headers)
|
214
|
-
|
211
|
+
|
215
212
|
if response.is_a?(Net::HTTPSuccess)
|
216
213
|
response
|
217
214
|
else
|
218
215
|
raise SalesforceError.new(response)
|
219
216
|
end
|
220
217
|
end
|
221
|
-
|
218
|
+
|
222
219
|
def http_get(path, headers={})
|
223
220
|
path = "#{@api_path_prefix}#{path}"
|
224
|
-
|
221
|
+
|
225
222
|
headers = {'Content-Type' => 'application/xml'}.merge(headers)
|
226
|
-
|
223
|
+
|
227
224
|
if @session_id
|
228
225
|
headers['X-SFDC-Session'] = @session_id
|
229
226
|
end
|
230
|
-
|
227
|
+
|
231
228
|
response = https_request(self.instance_host).get(path, headers)
|
232
|
-
|
229
|
+
|
233
230
|
if response.is_a?(Net::HTTPSuccess)
|
234
231
|
response
|
235
232
|
else
|
236
233
|
raise SalesforceError.new(response)
|
237
234
|
end
|
238
235
|
end
|
239
|
-
|
236
|
+
|
240
237
|
def https_request(host)
|
241
238
|
req = Net::HTTP.new(host, 443)
|
242
239
|
req.use_ssl = true
|
243
240
|
req.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
244
241
|
req
|
245
242
|
end
|
246
|
-
|
243
|
+
|
247
244
|
def instance_id(url)
|
248
245
|
url.match(/:\/\/([a-zA-Z0-9\-\.]{2,}).salesforce/)[1]
|
249
246
|
end
|