salesforcebulk 1.4.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +92 -117
  3. data/lib/salesforce_bulk.rb +2 -10
  4. data/lib/salesforce_bulk/batch.rb +1 -1
  5. data/lib/salesforce_bulk/batch_result.rb +11 -11
  6. data/lib/salesforce_bulk/client.rb +91 -70
  7. data/lib/salesforce_bulk/job.rb +8 -8
  8. data/lib/salesforce_bulk/query_result_collection.rb +11 -11
  9. data/lib/salesforce_bulk/version.rb +1 -1
  10. metadata +21 -115
  11. data/.gitignore +0 -4
  12. data/.travis.yml +0 -10
  13. data/Gemfile +0 -3
  14. data/LICENSE +0 -20
  15. data/Rakefile +0 -22
  16. data/lib/salesforce_bulk/core_extensions/string.rb +0 -14
  17. data/salesforcebulk.gemspec +0 -30
  18. data/test/fixtures/batch_create_request.csv +0 -3
  19. data/test/fixtures/batch_create_response.xml +0 -13
  20. data/test/fixtures/batch_info_list_response.xml +0 -27
  21. data/test/fixtures/batch_info_response.xml +0 -14
  22. data/test/fixtures/batch_result_list_response.csv +0 -3
  23. data/test/fixtures/config.yml +0 -5
  24. data/test/fixtures/invalid_batch_error.xml +0 -5
  25. data/test/fixtures/invalid_error.xml +0 -5
  26. data/test/fixtures/invalid_job_error.xml +0 -5
  27. data/test/fixtures/invalid_session_error.xml +0 -5
  28. data/test/fixtures/job_abort_request.xml +0 -1
  29. data/test/fixtures/job_abort_response.xml +0 -25
  30. data/test/fixtures/job_close_request.xml +0 -1
  31. data/test/fixtures/job_close_response.xml +0 -25
  32. data/test/fixtures/job_create_request.xml +0 -1
  33. data/test/fixtures/job_create_response.xml +0 -25
  34. data/test/fixtures/job_info_response.xml +0 -25
  35. data/test/fixtures/login_error.xml +0 -1
  36. data/test/fixtures/login_request.xml +0 -1
  37. data/test/fixtures/login_response.xml +0 -39
  38. data/test/fixtures/query_result_list_response.xml +0 -1
  39. data/test/fixtures/query_result_response.csv +0 -5
  40. data/test/lib/test_batch.rb +0 -252
  41. data/test/lib/test_batch_result.rb +0 -36
  42. data/test/lib/test_core_extensions.rb +0 -15
  43. data/test/lib/test_initialization.rb +0 -80
  44. data/test/lib/test_job.rb +0 -247
  45. data/test/lib/test_query_result_collection.rb +0 -86
  46. data/test/test_helper.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b6f6595658ab7d407acd1f74cb96b8b40ae9a303
4
- data.tar.gz: d9750c85f6cad1d42a6c81f108d6de8fb1558748
2
+ SHA256:
3
+ metadata.gz: f820b68636330723af5423eac1fd9c0bbbc09cb22fe14651bf82f9b9a76108bf
4
+ data.tar.gz: af753d04858e10b980c5c140ab2e0bd251f33edd4e9382f96c0112d40a16f0e4
5
5
  SHA512:
6
- metadata.gz: abe409976e507aa19ff46d3519088e84656f039aa1521def05daa495f114fe4c04bf4835f47baabcc69bf12da52ee3198cd1a950a0fd30eb472bea336acf2dab
7
- data.tar.gz: 87ec0fa2cd572bdf23f624099bfa6ea43c512835672bff6e2aead8be09f4d05a7dce574fcbfaa9cc2285d99c18aaa2a39ff8469eb2726be02aad74636607993e
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). This gem was built on Ruby 1.8.7, 1.9.2, and 1.9.3.
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
- gem install salesforcebulk
14
+ ```
15
+ gem install salesforcebulk
16
+ ```
12
17
 
13
18
  Or include it in your project's `Gemfile` with Bundler:
14
19
 
15
- gem 'salesforcebulk'
20
+ ```ruby
21
+ gem 'salesforcebulk'
22
+ ```
16
23
 
17
24
  ## Contribute
18
25
 
19
- To contribute, fork this repo, create a topic branch, make changes, then send a pull request. Pull requests without accompanying tests will *not* be accepted. If you need help creating tests let me know and I'll help out. To setup the project and run tests in your fork, just do:
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
- bundle install
22
- rake
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
- require 'salesforce_bulk'
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
- username: MyUsername
43
- password: MyPassword
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
- Then in a Ruby script:
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
- data1 = [{:Name__c => 'Test 1'}, {:Name__c => 'Test 2'}]
61
- data2 = [{:Name__c => 'Test 3'}, {:Name__c => 'Test 4'}]
62
-
63
- job = client.add_job(:insert, :MyObject__c)
64
-
65
- # easily add multiple batches to a job
66
- batch = client.add_batch(job.id, data1)
67
- batch = client.add_batch(job.id, data2)
68
-
69
- job = client.close_job(job.id) # or use the abort_job(id) method
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
- - :delete
75
- - :insert
76
- - :update
77
- - :upsert
78
- - :query
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
- job = client.add_job(:upsert, :MyObject__c, :external_id_field_name => :MyId__c)
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
- job = client.add_job(:upsert, :MyObject__c, :concurrency_mode => :Serial, :external_id_field_name => :MyId__c)
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
- job = client.job_info(jobId) # returns a Job object
93
-
94
- puts "Job #{job.id} is closed." if job.closed? # other: open?, aborted?
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
- batch = client.batch_info(jobId, batchId) # returns a Batch object
101
-
102
- puts "Batch #{batch.id} is in progress." if batch.in_progress?
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
- batches = client.batch_info_list(jobId) # returns an Array of Batch objects
107
-
108
- batches.each do |batch|
109
- puts "Batch #{batch.id} completed." if batch.completed? # other: failed?, in_progress?, queued?
110
- end
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
- results = client.batch_result(jobId, batchId) # returns an Array of BatchResult objects
119
-
120
- results.each do |result|
121
- puts "Item #{result.id} had an error of: #{result.error}" if result.error?
122
- end
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
- # returns a QueryResultCollection object (an Array)
131
- results = client.batch_result(jobId, batchId)
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
- 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.
142
+ while results.any?
147
143
 
148
- ## Contribution Suggestions/Ideas
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
- - Support for other Ruby platforms
151
- - Clean up/reorganize tests better
152
- - Rdocs
149
+ puts "Another set is available." if results.next?
153
150
 
154
- ## Version History
151
+ results.next
155
152
 
156
- **1.4.0** (June 1, 2014)
153
+ end
154
+ ```
157
155
 
158
- * Added state_message to Batch class (#11 - thanks [@bethesque](https://github.com/bethesque))
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
- **1.3.0** (April 28, 2014)
158
+ ## Releasing
161
159
 
162
- * Added support for multiple subdomains (#10 - thanks [@lucianapazos](https://github.com/lucianapazos))
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
- **1.2.0** (October 10, 2012)
162
+ ## Contribution Suggestions/Ideas
166
163
 
167
- * Added Ruby 1.8.7 support (thanks [@dlee](https://github.com/dlee))
164
+ - Support for other Ruby platforms
165
+ - Clean up/reorganize tests better
166
+ - Rdocs
168
167
 
169
- **1.1.0** (August 20, 2012)
168
+ ### Releasing
170
169
 
171
- * Added travis setup. Support for Ruby 1.9.2 and 1.9.3 specified.
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
- **1.0.0** (August 17, 2012)
172
+ ## Contributing
177
173
 
178
- * Initial public release.
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
- (The MIT license)
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).
@@ -1,15 +1,7 @@
1
- require 'net/https'
1
+ require 'net/http'
2
2
  require 'xmlsimple'
3
- if RUBY_VERSION < '1.9'
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'
@@ -46,7 +46,7 @@ module SalesforceBulk
46
46
  end
47
47
 
48
48
  def state?(value)
49
- self.state.present? && self.state.casecmp(value) == 0
49
+ !self.state.nil? && self.state.casecmp(value) == 0
50
50
  end
51
51
  end
52
52
  end
@@ -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.present?
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
- options.assert_valid_keys(:username, :password, :login_host, :version)
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
- options.assert_valid_keys(:external_id_field_name, :concurrency_mode)
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
- job = Job.new_from_xml(data)
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.body =~ /<.*?>/m
146
+
147
+ if ['application/xml', 'text/xml'].include? response.content_type
149
148
  result = XmlSimple.xml_in(response.body)
150
-
151
- if result['result'].present?
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].to_b, row[2].to_b, row[3])
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.to_b if s.kind_of? String }]) do |row|
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