salesforcebulk 1.4.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +92 -117
- data/lib/salesforce_bulk.rb +2 -10
- data/lib/salesforce_bulk/batch.rb +1 -1
- data/lib/salesforce_bulk/batch_result.rb +11 -11
- data/lib/salesforce_bulk/client.rb +91 -70
- data/lib/salesforce_bulk/job.rb +8 -8
- data/lib/salesforce_bulk/query_result_collection.rb +11 -11
- data/lib/salesforce_bulk/version.rb +1 -1
- metadata +21 -115
- data/.gitignore +0 -4
- data/.travis.yml +0 -10
- data/Gemfile +0 -3
- data/LICENSE +0 -20
- data/Rakefile +0 -22
- data/lib/salesforce_bulk/core_extensions/string.rb +0 -14
- 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
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f820b68636330723af5423eac1fd9c0bbbc09cb22fe14651bf82f9b9a76108bf
|
4
|
+
data.tar.gz: af753d04858e10b980c5c140ab2e0bd251f33edd4e9382f96c0112d40a16f0e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 880925281c6aded2a647d9ad59eed9d9f5d2459c8093e9616f8569ee675b7562e8504663e64421e7d4d8ba3e7c73db0be534de75fe41af082b15cfcd5f08fa3e
|
7
|
+
data.tar.gz: 7987781dfd889b6b27f0ae32bf13e49f83a3e1f8c1872c42ca47bb0c33605b13aeeb7d71002b8d02f9072a44d8f76b665b29254ffc8ae022832d4fa48a85aa88
|
data/README.md
CHANGED
@@ -1,25 +1,34 @@
|
|
1
1
|
# SalesforceBulk
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/salesforcebulk.svg)](https://badge.fury.io/rb/salesforcebulk)
|
4
|
+
[![Tests](https://github.com/javierjulio/salesforce_bulk/actions/workflows/ci.yml/badge.svg)](https://github.com/javierjulio/salesforce_bulk/actions/workflows/ci.yml)
|
5
|
+
|
3
6
|
## Overview
|
4
7
|
|
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).
|
8
|
+
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).
|
6
9
|
|
7
10
|
## Installation
|
8
11
|
|
9
12
|
Install SalesforceBulk from RubyGems:
|
10
13
|
|
11
|
-
|
14
|
+
```
|
15
|
+
gem install salesforcebulk
|
16
|
+
```
|
12
17
|
|
13
18
|
Or include it in your project's `Gemfile` with Bundler:
|
14
19
|
|
15
|
-
|
20
|
+
```ruby
|
21
|
+
gem 'salesforcebulk'
|
22
|
+
```
|
16
23
|
|
17
24
|
## Contribute
|
18
25
|
|
19
|
-
To contribute, fork this repo, create a topic branch,
|
26
|
+
To contribute, fork this repo, create a topic branch, add changes and tests, then send a pull request. To setup the project and run tests in your fork, just do:
|
20
27
|
|
21
|
-
|
22
|
-
|
28
|
+
```
|
29
|
+
bundle install
|
30
|
+
bundle exec rake
|
31
|
+
```
|
23
32
|
|
24
33
|
## Configuration and Initialization
|
25
34
|
|
@@ -27,29 +36,14 @@ To contribute, fork this repo, create a topic branch, make changes, then send a
|
|
27
36
|
|
28
37
|
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
38
|
|
30
|
-
|
31
|
-
|
32
|
-
client = SalesforceBulk::Client.new(username: 'MyUsername', password: 'MyPasswordWithSecurtyToken')
|
33
|
-
client.authenticate
|
34
|
-
|
35
|
-
Optional keys include `login_host` (default is 'login.salesforce.com') and `version` (default is '24.0').
|
36
|
-
|
37
|
-
### Configuring from a YAML file
|
38
|
-
|
39
|
-
Create a YAML file with the content below. Only `username` and `password` is required.
|
39
|
+
```ruby
|
40
|
+
require 'salesforce_bulk'
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
login_host: login.salesforce.com # default
|
45
|
-
version: 24.0 # default
|
42
|
+
client = SalesforceBulk::Client.new(username: 'MyUsername', password: 'MyPasswordWithSecurtyToken')
|
43
|
+
client.authenticate
|
44
|
+
```
|
46
45
|
|
47
|
-
|
48
|
-
|
49
|
-
require 'salesforce_bulk'
|
50
|
-
|
51
|
-
client = SalesforceBulk::Client.new("config/salesforce_bulk.yml")
|
52
|
-
client.authenticate
|
46
|
+
Optional keys include `login_host` (default is 'login.salesforce.com') and `version` (default is '24.0').
|
53
47
|
|
54
48
|
## Usage Examples
|
55
49
|
|
@@ -57,57 +51,69 @@ An important note about the data in any of the examples below: each hash in a da
|
|
57
51
|
|
58
52
|
### Basic Overall Example
|
59
53
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
54
|
+
```ruby
|
55
|
+
data1 = [{:Name__c => 'Test 1'}, {:Name__c => 'Test 2'}]
|
56
|
+
data2 = [{:Name__c => 'Test 3'}, {:Name__c => 'Test 4'}]
|
57
|
+
|
58
|
+
job = client.add_job(:insert, :MyObject__c)
|
59
|
+
|
60
|
+
# easily add multiple batches to a job
|
61
|
+
batch = client.add_batch(job.id, data1)
|
62
|
+
batch = client.add_batch(job.id, data2)
|
63
|
+
|
64
|
+
job = client.close_job(job.id) # or use the abort_job(id) method
|
65
|
+
```
|
70
66
|
|
71
67
|
### Adding a Job
|
72
68
|
|
73
69
|
When adding a job you can specify the following operations for the first argument:
|
74
|
-
-
|
75
|
-
-
|
76
|
-
-
|
77
|
-
-
|
78
|
-
-
|
70
|
+
- `:delete`
|
71
|
+
- `:insert`
|
72
|
+
- `:update`
|
73
|
+
- `:upsert`
|
74
|
+
- `:query`
|
79
75
|
|
80
76
|
When using the :upsert operation you must specify an external ID field name:
|
81
77
|
|
82
|
-
|
78
|
+
```ruby
|
79
|
+
job = client.add_job(:upsert, :MyObject__c, :external_id_field_name => :MyId__c)
|
80
|
+
```
|
83
81
|
|
84
82
|
For any operation you should be able to specify a concurrency mode. The default is `Parallel`. The only other choice is `Serial`.
|
85
83
|
|
86
|
-
|
84
|
+
```ruby
|
85
|
+
job = client.add_job(:upsert, :MyObject__c, :concurrency_mode => :Serial, :external_id_field_name => :MyId__c)
|
86
|
+
```
|
87
87
|
|
88
88
|
### Retrieving Job Information (e.g. Status)
|
89
89
|
|
90
90
|
The Job object has various properties such as status, created time, number of completed and failed batches and various other values.
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
92
|
+
```ruby
|
93
|
+
job = client.job_info(jobId) # returns a Job object
|
94
|
+
|
95
|
+
puts "Job #{job.id} is closed." if job.closed? # other: open?, aborted?
|
96
|
+
```
|
95
97
|
|
96
98
|
### Retrieving Info for a single Batch
|
97
99
|
|
98
100
|
The Batch object has various properties such as status, created time, number of processed and failed records and various other values.
|
99
101
|
|
100
|
-
|
101
|
-
|
102
|
-
|
102
|
+
```ruby
|
103
|
+
batch = client.batch_info(jobId, batchId) # returns a Batch object
|
104
|
+
|
105
|
+
puts "Batch #{batch.id} is in progress." if batch.in_progress?
|
106
|
+
```
|
103
107
|
|
104
108
|
### Retrieving Info for all Batches
|
105
109
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
110
|
+
```ruby
|
111
|
+
batches = client.batch_info_list(jobId) # returns an Array of Batch objects
|
112
|
+
|
113
|
+
batches.each do |batch|
|
114
|
+
puts "Batch #{batch.id} completed." if batch.completed? # other: failed?, in_progress?, queued?
|
115
|
+
end
|
116
|
+
```
|
111
117
|
|
112
118
|
### Retrieving Batch Results (for Delete, Insert, Update and Upsert)
|
113
119
|
|
@@ -115,11 +121,13 @@ To verify that a batch completed successfully or failed call the `batch_info` or
|
|
115
121
|
|
116
122
|
The object returned from the following example only applies to the operations: `delete`, `insert`, `update` and `upsert`. Query results are handled differently.
|
117
123
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
124
|
+
```ruby
|
125
|
+
results = client.batch_result(jobId, batchId) # returns an Array of BatchResult objects
|
126
|
+
|
127
|
+
results.each do |result|
|
128
|
+
puts "Item #{result.id} had an error of: #{result.error}" if result.error?
|
129
|
+
end
|
130
|
+
```
|
123
131
|
|
124
132
|
### Retrieving Query based Batch Results
|
125
133
|
|
@@ -127,77 +135,44 @@ To verify that a batch completed successfully or failed call the `batch_info` or
|
|
127
135
|
|
128
136
|
Query results are handled differently as its possible that a single batch could return multiple results if objects returned are large enough. Note: I haven't been able to replicate this behavior but in a fork by @WWJacob has [discovered that multiple results can be returned](https://github.com/WWJacob/salesforce_bulk/commit/8f9e68c390230e885823e45cd2616ac3159697ef).
|
129
137
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
while results.any?
|
134
|
-
|
135
|
-
# Assuming query was: SELECT Id, Name, CustomField__c FROM Account
|
136
|
-
results.each do |result|
|
137
|
-
puts result[:Id], result[:Name], result[:CustomField__c]
|
138
|
-
end
|
139
|
-
|
140
|
-
puts "Another set is available." if results.next?
|
141
|
-
|
142
|
-
results.next
|
143
|
-
|
144
|
-
end
|
138
|
+
```ruby
|
139
|
+
# returns a QueryResultCollection object (an Array)
|
140
|
+
results = client.batch_result(jobId, batchId)
|
145
141
|
|
146
|
-
|
142
|
+
while results.any?
|
147
143
|
|
148
|
-
|
144
|
+
# Assuming query was: SELECT Id, Name, CustomField__c FROM Account
|
145
|
+
results.each do |result|
|
146
|
+
puts result[:Id], result[:Name], result[:CustomField__c]
|
147
|
+
end
|
149
148
|
|
150
|
-
|
151
|
-
- Clean up/reorganize tests better
|
152
|
-
- Rdocs
|
149
|
+
puts "Another set is available." if results.next?
|
153
150
|
|
154
|
-
|
151
|
+
results.next
|
155
152
|
|
156
|
-
|
153
|
+
end
|
154
|
+
```
|
157
155
|
|
158
|
-
|
156
|
+
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.
|
159
157
|
|
160
|
-
|
158
|
+
## Releasing
|
161
159
|
|
162
|
-
|
163
|
-
* Added dependency version requirements to gemspec
|
160
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
164
161
|
|
165
|
-
|
162
|
+
## Contribution Suggestions/Ideas
|
166
163
|
|
167
|
-
|
164
|
+
- Support for other Ruby platforms
|
165
|
+
- Clean up/reorganize tests better
|
166
|
+
- Rdocs
|
168
167
|
|
169
|
-
|
168
|
+
### Releasing
|
170
169
|
|
171
|
-
|
172
|
-
* Removed `token` property on Client object. Specify token in `password` field.
|
173
|
-
* Accepted pull request for 1.9.3 improvements.
|
174
|
-
* Description updates in README.
|
170
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
175
171
|
|
176
|
-
|
172
|
+
## Contributing
|
177
173
|
|
178
|
-
|
174
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/javierjulio/salesforce_bulk. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
179
175
|
|
180
176
|
## License
|
181
177
|
|
182
|
-
|
183
|
-
|
184
|
-
Copyright (c) 2012 Javier Julio
|
185
|
-
|
186
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
187
|
-
a copy of this software and associated documentation files (the
|
188
|
-
"Software"), to deal in the Software without restriction, including
|
189
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
190
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
191
|
-
permit persons to whom the Software is furnished to do so, subject to
|
192
|
-
the following conditions:
|
193
|
-
|
194
|
-
The above copyright notice and this permission notice shall be
|
195
|
-
included in all copies or substantial portions of the Software.
|
196
|
-
|
197
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
198
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
199
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
200
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
201
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
202
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
203
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
178
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/salesforce_bulk.rb
CHANGED
@@ -1,15 +1,7 @@
|
|
1
|
-
require 'net/
|
1
|
+
require 'net/http'
|
2
2
|
require 'xmlsimple'
|
3
|
-
|
4
|
-
require 'fastercsv'
|
5
|
-
else
|
6
|
-
require 'csv'
|
7
|
-
end
|
8
|
-
require 'active_support'
|
9
|
-
require 'active_support/core_ext/object/blank'
|
10
|
-
require 'active_support/core_ext/hash/keys'
|
3
|
+
require 'csv'
|
11
4
|
require 'salesforce_bulk/version'
|
12
|
-
require 'salesforce_bulk/core_extensions/string'
|
13
5
|
require 'salesforce_bulk/salesforce_error'
|
14
6
|
require 'salesforce_bulk/client'
|
15
7
|
require 'salesforce_bulk/job'
|
@@ -1,37 +1,37 @@
|
|
1
1
|
module SalesforceBulk
|
2
2
|
class BatchResult
|
3
|
-
|
3
|
+
|
4
4
|
# A boolean indicating if record was created. If updated value is false.
|
5
5
|
attr_accessor :created
|
6
|
-
|
6
|
+
|
7
7
|
# The error message.
|
8
8
|
attr_accessor :error
|
9
|
-
|
9
|
+
|
10
10
|
# The record's unique id.
|
11
11
|
attr_accessor :id
|
12
|
-
|
13
|
-
# If record was created successfully. If false then an error message is provided.
|
12
|
+
|
13
|
+
# If record was created successfully. If false then an error message is provided.
|
14
14
|
attr_accessor :success
|
15
|
-
|
15
|
+
|
16
16
|
def initialize(id, success, created, error)
|
17
17
|
@id = id
|
18
18
|
@success = success
|
19
19
|
@created = created
|
20
20
|
@error = error
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def error?
|
24
|
-
error.
|
24
|
+
!error.nil? && error.respond_to?(:empty?) && !error.empty?
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
def created?
|
28
28
|
created
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
def successful?
|
32
32
|
success
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
def updated?
|
36
36
|
!created && success
|
37
37
|
end
|
@@ -1,46 +1,45 @@
|
|
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
|
+
|
20
|
+
# The ID for authenticated session
|
21
|
+
attr_reader :session_id
|
22
|
+
|
23
23
|
def initialize(options={})
|
24
|
-
if options.is_a?(String)
|
25
|
-
options = YAML.load_file(options)
|
26
|
-
options.symbolize_keys!
|
27
|
-
end
|
28
|
-
|
29
24
|
options = {:login_host => 'login.salesforce.com', :version => 24.0}.merge(options)
|
30
|
-
|
31
|
-
|
32
|
-
|
25
|
+
|
26
|
+
assert_valid_keys(options, :username, :password, :login_host, :version)
|
27
|
+
|
33
28
|
self.username = options[:username]
|
34
29
|
self.password = "#{options[:password]}"
|
35
30
|
self.login_host = options[:login_host]
|
36
31
|
self.version = options[:version]
|
37
|
-
|
32
|
+
|
38
33
|
@api_path_prefix = "/services/async/#{version}/"
|
39
34
|
@valid_operations = [:delete, :insert, :update, :upsert, :query]
|
40
35
|
@valid_concurrency_modes = ['Parallel', 'Serial']
|
41
36
|
end
|
42
|
-
|
37
|
+
|
43
38
|
def authenticate
|
39
|
+
# Clear session attributes just in case client already had a session
|
40
|
+
@session_id = nil
|
41
|
+
self.instance_host = nil
|
42
|
+
|
44
43
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
45
44
|
xml += '<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
|
46
45
|
xml += ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
@@ -52,63 +51,63 @@ module SalesforceBulk
|
|
52
51
|
xml += "</n1:login>"
|
53
52
|
xml += "</env:Body>"
|
54
53
|
xml += "</env:Envelope>\n"
|
55
|
-
|
54
|
+
|
56
55
|
response = http_post("/services/Soap/u/#{version}", xml, 'Content-Type' => 'text/xml', 'SOAPAction' => 'login')
|
57
|
-
|
56
|
+
|
58
57
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
59
58
|
result = data['Body']['loginResponse']['result']
|
60
|
-
|
59
|
+
|
61
60
|
@session_id = result['sessionId']
|
62
|
-
|
61
|
+
|
63
62
|
self.instance_host = "#{instance_id(result['serverUrl'])}.salesforce.com"
|
64
63
|
self
|
65
64
|
end
|
66
|
-
|
65
|
+
|
67
66
|
def abort_job(jobId)
|
68
67
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
69
68
|
xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
|
70
69
|
xml += "<state>Aborted</state>"
|
71
70
|
xml += "</jobInfo>"
|
72
|
-
|
71
|
+
|
73
72
|
response = http_post("job/#{jobId}", xml)
|
74
73
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
75
74
|
Job.new_from_xml(data)
|
76
75
|
end
|
77
|
-
|
76
|
+
|
78
77
|
def add_batch(jobId, data)
|
79
78
|
body = data
|
80
|
-
|
79
|
+
|
81
80
|
if data.is_a?(Array)
|
82
81
|
raise ArgumentError, "Data set exceeds 10000 record limit by #{data.length - 10000}" if data.length > 10000
|
83
|
-
|
82
|
+
|
84
83
|
keys = data.first.keys
|
85
84
|
body = keys.to_csv
|
86
|
-
|
85
|
+
|
87
86
|
data.each do |item|
|
88
87
|
item_values = keys.map { |key| item[key] }
|
89
88
|
body += item_values.to_csv
|
90
89
|
end
|
91
90
|
end
|
92
|
-
|
93
|
-
# Despite the content for a query operation batch being plain text we
|
91
|
+
|
92
|
+
# Despite the content for a query operation batch being plain text we
|
94
93
|
# still have to specify CSV content type per API docs.
|
95
94
|
response = http_post("job/#{jobId}/batch", body, "Content-Type" => "text/csv; charset=UTF-8")
|
96
95
|
result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
97
96
|
Batch.new_from_xml(result)
|
98
97
|
end
|
99
|
-
|
98
|
+
|
100
99
|
def add_job(operation, sobject, options={})
|
101
100
|
operation = operation.to_s.downcase.to_sym
|
102
|
-
|
101
|
+
|
103
102
|
raise ArgumentError.new("Invalid operation: #{operation}") unless @valid_operations.include?(operation)
|
104
|
-
|
105
|
-
|
106
|
-
|
103
|
+
|
104
|
+
assert_valid_keys(options, :external_id_field_name, :concurrency_mode)
|
105
|
+
|
107
106
|
if options[:concurrency_mode]
|
108
107
|
concurrency_mode = options[:concurrency_mode].capitalize
|
109
108
|
raise ArgumentError.new("Invalid concurrency mode: #{concurrency_mode}") unless @valid_concurrency_modes.include?(concurrency_mode)
|
110
109
|
end
|
111
|
-
|
110
|
+
|
112
111
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
113
112
|
xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
|
114
113
|
xml += "<operation>#{operation}</operation>"
|
@@ -117,16 +116,16 @@ module SalesforceBulk
|
|
117
116
|
xml += "<concurrencyMode>#{options[:concurrency_mode]}</concurrencyMode>" if options[:concurrency_mode]
|
118
117
|
xml += "<contentType>CSV</contentType>"
|
119
118
|
xml += "</jobInfo>"
|
120
|
-
|
119
|
+
|
121
120
|
response = http_post("job", xml)
|
122
121
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
123
|
-
|
122
|
+
Job.new_from_xml(data)
|
124
123
|
end
|
125
|
-
|
124
|
+
|
126
125
|
def batch_info_list(jobId)
|
127
126
|
response = http_get("job/#{jobId}/batch")
|
128
127
|
result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
129
|
-
|
128
|
+
|
130
129
|
if result['batchInfo'].is_a?(Array)
|
131
130
|
result['batchInfo'].collect do |info|
|
132
131
|
Batch.new_from_xml(info)
|
@@ -135,73 +134,73 @@ module SalesforceBulk
|
|
135
134
|
[Batch.new_from_xml(result['batchInfo'])]
|
136
135
|
end
|
137
136
|
end
|
138
|
-
|
137
|
+
|
139
138
|
def batch_info(jobId, batchId)
|
140
139
|
response = http_get("job/#{jobId}/batch/#{batchId}")
|
141
140
|
result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
142
141
|
Batch.new_from_xml(result)
|
143
142
|
end
|
144
|
-
|
143
|
+
|
145
144
|
def batch_result(jobId, batchId)
|
146
145
|
response = http_get("job/#{jobId}/batch/#{batchId}/result")
|
147
|
-
|
148
|
-
if response.
|
146
|
+
|
147
|
+
if ['application/xml', 'text/xml'].include? response.content_type
|
149
148
|
result = XmlSimple.xml_in(response.body)
|
150
|
-
|
151
|
-
if result['result'].
|
149
|
+
|
150
|
+
if !result['result'].nil? && !result['result'].empty?
|
152
151
|
results = query_result(jobId, batchId, result['result'].first)
|
153
|
-
|
152
|
+
|
154
153
|
collection = QueryResultCollection.new(self, jobId, batchId, result['result'].first, result['result'])
|
155
154
|
collection.replace(results)
|
156
155
|
end
|
157
156
|
else
|
158
157
|
result = BatchResultCollection.new(jobId, batchId)
|
159
|
-
|
158
|
+
|
160
159
|
CSV.parse(response.body, :headers => true) do |row|
|
161
|
-
result << BatchResult.new(row[0], row[1]
|
160
|
+
result << BatchResult.new(row[0], to_boolean(row[1]), to_boolean(row[2]), row[3])
|
162
161
|
end
|
163
|
-
|
162
|
+
|
164
163
|
result
|
165
164
|
end
|
166
165
|
end
|
167
|
-
|
166
|
+
|
168
167
|
def query_result(job_id, batch_id, result_id)
|
169
168
|
headers = {"Content-Type" => "text/csv; charset=UTF-8"}
|
170
169
|
response = http_get("job/#{job_id}/batch/#{batch_id}/result/#{result_id}", headers)
|
171
|
-
|
170
|
+
|
172
171
|
lines = response.body.lines.to_a
|
173
172
|
headers = CSV.parse_line(lines.shift).collect { |header| header.to_sym }
|
174
|
-
|
173
|
+
|
175
174
|
result = []
|
176
|
-
|
177
|
-
#CSV.parse(lines.join, :headers => headers, :converters => [:all, lambda{|s| s
|
175
|
+
|
176
|
+
#CSV.parse(lines.join, :headers => headers, :converters => [:all, lambda{|s| to_boolean(s) if s.kind_of? String }]) do |row|
|
178
177
|
CSV.parse(lines.join, :headers => headers) do |row|
|
179
178
|
result << Hash[row.headers.zip(row.fields)]
|
180
179
|
end
|
181
|
-
|
180
|
+
|
182
181
|
result
|
183
182
|
end
|
184
|
-
|
183
|
+
|
185
184
|
def close_job(jobId)
|
186
185
|
xml = '<?xml version="1.0" encoding="utf-8"?>'
|
187
186
|
xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
|
188
187
|
xml += "<state>Closed</state>"
|
189
188
|
xml += "</jobInfo>"
|
190
|
-
|
189
|
+
|
191
190
|
response = http_post("job/#{jobId}", xml)
|
192
191
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
193
192
|
Job.new_from_xml(data)
|
194
193
|
end
|
195
|
-
|
194
|
+
|
196
195
|
def job_info(jobId)
|
197
196
|
response = http_get("job/#{jobId}")
|
198
197
|
data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
|
199
198
|
Job.new_from_xml(data)
|
200
199
|
end
|
201
|
-
|
200
|
+
|
202
201
|
def http_post(path, body, headers={})
|
203
202
|
headers = {'Content-Type' => 'application/xml'}.merge(headers)
|
204
|
-
|
203
|
+
|
205
204
|
if @session_id
|
206
205
|
headers['X-SFDC-Session'] = @session_id
|
207
206
|
host = instance_host
|
@@ -209,43 +208,65 @@ module SalesforceBulk
|
|
209
208
|
else
|
210
209
|
host = self.login_host
|
211
210
|
end
|
212
|
-
|
211
|
+
|
213
212
|
response = https_request(host).post(path, body, headers)
|
214
|
-
|
213
|
+
|
215
214
|
if response.is_a?(Net::HTTPSuccess)
|
216
215
|
response
|
217
216
|
else
|
218
217
|
raise SalesforceError.new(response)
|
219
218
|
end
|
220
219
|
end
|
221
|
-
|
220
|
+
|
222
221
|
def http_get(path, headers={})
|
223
222
|
path = "#{@api_path_prefix}#{path}"
|
224
|
-
|
223
|
+
|
225
224
|
headers = {'Content-Type' => 'application/xml'}.merge(headers)
|
226
|
-
|
225
|
+
|
227
226
|
if @session_id
|
228
227
|
headers['X-SFDC-Session'] = @session_id
|
229
228
|
end
|
230
|
-
|
229
|
+
|
231
230
|
response = https_request(self.instance_host).get(path, headers)
|
232
|
-
|
231
|
+
|
233
232
|
if response.is_a?(Net::HTTPSuccess)
|
234
233
|
response
|
235
234
|
else
|
236
235
|
raise SalesforceError.new(response)
|
237
236
|
end
|
238
237
|
end
|
239
|
-
|
238
|
+
|
240
239
|
def https_request(host)
|
241
240
|
req = Net::HTTP.new(host, 443)
|
242
241
|
req.use_ssl = true
|
243
242
|
req.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
244
243
|
req
|
245
244
|
end
|
246
|
-
|
245
|
+
|
247
246
|
def instance_id(url)
|
248
247
|
url.match(/:\/\/([a-zA-Z0-9\-\.]{2,}).salesforce/)[1]
|
249
248
|
end
|
249
|
+
|
250
|
+
private
|
251
|
+
|
252
|
+
def assert_valid_keys(options, *valid_keys)
|
253
|
+
valid_keys.flatten!
|
254
|
+
options.each_key do |k|
|
255
|
+
unless valid_keys.include?(k)
|
256
|
+
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def to_boolean(value)
|
262
|
+
if !value.nil?
|
263
|
+
if value.strip.casecmp("true") == 0
|
264
|
+
return true
|
265
|
+
elsif value.strip.casecmp("false") == 0
|
266
|
+
return false
|
267
|
+
end
|
268
|
+
end
|
269
|
+
value
|
270
|
+
end
|
250
271
|
end
|
251
272
|
end
|