salesforce_bulk_api 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.env.sample +7 -0
- data/.github/workflows/ci.yml +36 -0
- data/.gitignore +1 -0
- data/.rspec +1 -1
- data/.rubocop.yml +1927 -0
- data/CHANGELOG.md +0 -0
- data/LICENCE +1 -1
- data/README.md +426 -71
- data/Rakefile +3 -3
- data/lib/salesforce_bulk_api/concerns/throttling.rb +1 -3
- data/lib/salesforce_bulk_api/connection.rb +12 -14
- data/lib/salesforce_bulk_api/job.rb +83 -85
- data/lib/salesforce_bulk_api/version.rb +1 -1
- data/lib/salesforce_bulk_api.rb +21 -22
- data/salesforce_bulk_api.gemspec +20 -18
- data/spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb +100 -143
- data/spec/spec_helper.rb +7 -4
- metadata +81 -12
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENCE
CHANGED
data/README.md
CHANGED
@@ -1,141 +1,496 @@
|
|
1
1
|
# Salesforce-Bulk-Api
|
2
|
+
|
2
3
|
[](http://badge.fury.io/rb/salesforce_bulk_api)
|
3
4
|
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Overview](#overview)
|
8
|
+
- [Installation](#installation)
|
9
|
+
- [Authentication](#authentication)
|
10
|
+
- [Usage](#usage)
|
11
|
+
- [Basic Operations](#basic-operations)
|
12
|
+
- [Method Parameters](#method-parameters)
|
13
|
+
- [Getting Results](#getting-results)
|
14
|
+
- [Null Value Handling](#null-value-handling)
|
15
|
+
- [Job Management](#job-management)
|
16
|
+
- [Batch Operations](#batch-operations)
|
17
|
+
- [Event Listening](#event-listening)
|
18
|
+
- [API Call Throttling](#api-call-throttling)
|
19
|
+
- [Monitoring and Counters](#monitoring-and-counters)
|
20
|
+
- [Error Handling](#error-handling)
|
21
|
+
- [Advanced Features](#advanced-features)
|
22
|
+
- [Contributing](#contributing)
|
23
|
+
- [License](#license)
|
24
|
+
|
4
25
|
## Overview
|
5
26
|
|
6
|
-
`SalesforceBulkApi` is a
|
7
|
-
It is rewritten from [salesforce_bulk](https://github.com/jorgevaldivia/salesforce_bulk).
|
8
|
-
It adds some missing features of `salesforce_bulk`.
|
27
|
+
`SalesforceBulkApi` is a Ruby wrapper for the Salesforce Bulk API. It is rewritten from [salesforce_bulk](https://github.com/jorgevaldivia/salesforce_bulk) and adds several missing features, making it easier to perform bulk operations with Salesforce from Ruby applications.
|
9
28
|
|
10
|
-
|
29
|
+
Key features:
|
30
|
+
- Support for all Bulk API operations (create, update, upsert, delete, query)
|
31
|
+
- Comprehensive error handling
|
32
|
+
- Job and batch status monitoring
|
33
|
+
- Event listening for job lifecycle
|
34
|
+
- API call throttling and monitoring
|
35
|
+
- Performance optimized string concatenation for large batches
|
11
36
|
|
12
|
-
|
37
|
+
## Installation
|
13
38
|
|
14
|
-
|
39
|
+
Add this line to your application's Gemfile:
|
15
40
|
|
16
|
-
|
41
|
+
```ruby
|
42
|
+
gem 'salesforce_bulk_api'
|
43
|
+
```
|
17
44
|
|
18
|
-
|
45
|
+
And then execute:
|
19
46
|
|
20
|
-
|
47
|
+
```
|
48
|
+
bundle install
|
49
|
+
```
|
50
|
+
|
51
|
+
Or install it directly:
|
21
52
|
|
22
|
-
|
53
|
+
```
|
54
|
+
gem install salesforce_bulk_api
|
55
|
+
```
|
23
56
|
|
24
|
-
|
57
|
+
## Authentication
|
25
58
|
|
26
|
-
|
59
|
+
You can authenticate with Salesforce using either `databasedotcom` or `restforce` gems. Both support various authentication methods including username/password, OmniAuth, and OAuth2.
|
27
60
|
|
28
|
-
|
29
|
-
[Restforce](https://github.com/ejholmes/restforce)
|
61
|
+
Please refer to the documentation of these gems for detailed authentication options:
|
30
62
|
|
31
|
-
|
32
|
-
|
63
|
+
- [Databasedotcom](https://github.com/heroku/databasedotcom)
|
64
|
+
- [Restforce](https://github.com/ejholmes/restforce)
|
65
|
+
|
66
|
+
### Authentication Examples
|
67
|
+
|
68
|
+
#### Using Databasedotcom:
|
33
69
|
|
34
70
|
```ruby
|
35
71
|
require 'salesforce_bulk_api'
|
36
72
|
|
37
73
|
client = Databasedotcom::Client.new(
|
38
|
-
:
|
39
|
-
:
|
74
|
+
client_id: SFDC_APP_CONFIG["client_id"],
|
75
|
+
client_secret: SFDC_APP_CONFIG["client_secret"]
|
40
76
|
)
|
41
77
|
client.authenticate(
|
42
|
-
:
|
43
|
-
:
|
78
|
+
token: " ",
|
79
|
+
instance_url: "http://na1.salesforce.com"
|
44
80
|
)
|
45
81
|
|
46
82
|
salesforce = SalesforceBulkApi::Api.new(client)
|
47
83
|
```
|
48
84
|
|
49
|
-
|
85
|
+
#### Using Restforce:
|
50
86
|
|
51
87
|
```ruby
|
52
88
|
require 'salesforce_bulk_api'
|
89
|
+
|
53
90
|
client = Restforce.new(
|
54
|
-
username:
|
55
|
-
password:
|
91
|
+
username: SFDC_APP_CONFIG['SFDC_USERNAME'],
|
92
|
+
password: SFDC_APP_CONFIG['SFDC_PASSWORD'],
|
56
93
|
security_token: SFDC_APP_CONFIG['SFDC_SECURITY_TOKEN'],
|
57
|
-
client_id:
|
58
|
-
client_secret:
|
59
|
-
host:
|
94
|
+
client_id: SFDC_APP_CONFIG['SFDC_CLIENT_ID'],
|
95
|
+
client_secret: SFDC_APP_CONFIG['SFDC_CLIENT_SECRET'],
|
96
|
+
host: SFDC_APP_CONFIG['SFDC_HOST']
|
60
97
|
)
|
61
98
|
client.authenticate!
|
62
99
|
|
63
100
|
salesforce = SalesforceBulkApi::Api.new(client)
|
64
101
|
```
|
65
102
|
|
66
|
-
|
103
|
+
## Usage
|
104
|
+
|
105
|
+
### Basic Operations
|
106
|
+
|
107
|
+
#### Create/Insert Records
|
67
108
|
|
68
109
|
```ruby
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
# You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
|
74
|
-
records_to_insert.push(new_account)
|
110
|
+
new_account = { "name" => "Test Account", "type" => "Other" }
|
111
|
+
records_to_insert = [new_account]
|
112
|
+
|
113
|
+
# Basic usage
|
75
114
|
result = salesforce.create("Account", records_to_insert)
|
76
|
-
puts "result is: #{result.inspect}"
|
77
115
|
|
78
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
|
116
|
+
# With response and custom batch size
|
117
|
+
result = salesforce.create("Account", records_to_insert, true, false, [], 5000)
|
118
|
+
```
|
119
|
+
|
120
|
+
#### Update Records
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
updated_account = { "name" => "Test Account -- Updated", "id" => "a00A0001009zA2m" }
|
124
|
+
records_to_update = [updated_account]
|
125
|
+
|
126
|
+
# Basic usage
|
82
127
|
salesforce.update("Account", records_to_update)
|
83
128
|
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
129
|
+
# With null handling
|
130
|
+
salesforce.update("Account", records_to_update, true, true, ["Phone"])
|
131
|
+
```
|
132
|
+
|
133
|
+
#### Upsert Records
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
upserted_account = { "name" => "Test Account -- Upserted", "External_Field_Name" => "123456" }
|
137
|
+
records_to_upsert = [upserted_account]
|
138
|
+
|
139
|
+
# Basic usage
|
140
|
+
salesforce.upsert("Account", records_to_upsert, "External_Field_Name")
|
89
141
|
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
142
|
+
# With all options
|
143
|
+
result = salesforce.upsert("Account", records_to_upsert, "External_Field_Name", true, false, [], 10000, 3600)
|
144
|
+
```
|
145
|
+
|
146
|
+
#### Delete Records
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
deleted_account = { "id" => "a00A0001009zA2m" }
|
150
|
+
records_to_delete = [deleted_account]
|
151
|
+
|
152
|
+
# Basic usage
|
94
153
|
salesforce.delete("Account", records_to_delete)
|
95
154
|
|
96
|
-
#
|
97
|
-
|
155
|
+
# With response
|
156
|
+
result = salesforce.delete("Account", records_to_delete, true)
|
157
|
+
```
|
158
|
+
|
159
|
+
#### Query Records
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
result = salesforce.query("Account", "SELECT id, name, createddate FROM Account LIMIT 3")
|
163
|
+
puts "Records found: #{result["batches"][0]["response"].length}"
|
98
164
|
```
|
99
165
|
|
100
|
-
###
|
166
|
+
### Method Parameters
|
167
|
+
|
168
|
+
All bulk operation methods support additional parameters for fine-tuned control:
|
169
|
+
|
170
|
+
#### Complete Method Signatures:
|
101
171
|
|
102
172
|
```ruby
|
103
|
-
#
|
104
|
-
|
105
|
-
|
173
|
+
# CREATE
|
174
|
+
salesforce.create(sobject, records, get_response=false, send_nulls=false, no_null_list=[], batch_size=10000, timeout=1500)
|
175
|
+
|
176
|
+
# UPDATE
|
177
|
+
salesforce.update(sobject, records, get_response=false, send_nulls=false, no_null_list=[], batch_size=10000, timeout=1500)
|
178
|
+
|
179
|
+
# UPSERT
|
180
|
+
salesforce.upsert(sobject, records, external_field, get_response=false, send_nulls=false, no_null_list=[], batch_size=10000, timeout=1500)
|
181
|
+
|
182
|
+
# DELETE
|
183
|
+
salesforce.delete(sobject, records, get_response=false, batch_size=10000, timeout=1500)
|
184
|
+
|
185
|
+
# QUERY
|
186
|
+
salesforce.query(sobject, query_string, batch_size=10000, timeout=1500)
|
106
187
|
```
|
107
188
|
|
108
|
-
|
189
|
+
#### Parameter Descriptions:
|
190
|
+
|
191
|
+
- **`get_response`** (Boolean): Whether to return batch processing results (default: false)
|
192
|
+
- **`send_nulls`** (Boolean): Whether to send null/empty values to Salesforce (default: false)
|
193
|
+
- **`no_null_list`** (Array): Fields to exclude from null value handling when `send_nulls` is true
|
194
|
+
- **`batch_size`** (Integer): Number of records per batch (default: 10000, max: 10000)
|
195
|
+
- **`timeout`** (Integer): Timeout in seconds for job completion (default: 1500)
|
196
|
+
|
197
|
+
### Getting Results
|
198
|
+
|
199
|
+
When `get_response` is set to true, you'll receive detailed results:
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
result = salesforce.create("Account", records, true)
|
203
|
+
|
204
|
+
# Access job information
|
205
|
+
puts "Job ID: #{result['job_id']}"
|
206
|
+
puts "Job state: #{result['state']}"
|
207
|
+
|
208
|
+
# Access batch results
|
209
|
+
result["batches"].each_with_index do |batch, index|
|
210
|
+
puts "Batch #{index + 1}:"
|
211
|
+
puts " State: #{batch['state'][0]}"
|
212
|
+
puts " Records processed: #{batch['numberRecordsProcessed'][0]}"
|
213
|
+
|
214
|
+
if batch["response"]
|
215
|
+
batch["response"].each do |record|
|
216
|
+
if record["success"] == ["true"]
|
217
|
+
puts " ✓ Success: #{record['id'][0]}"
|
218
|
+
else
|
219
|
+
puts " ✗ Error: #{record['errors'][0]['message'][0]}"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
### Null Value Handling
|
227
|
+
|
228
|
+
Control how null and empty values are handled:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
records = [
|
232
|
+
{ "Id" => "001...", "Name" => "Test", "Phone" => "", "Website" => nil }
|
233
|
+
]
|
234
|
+
|
235
|
+
# Send nulls for empty/nil fields, except for Phone
|
236
|
+
result = salesforce.update("Account", records, true, true, ["Phone"])
|
237
|
+
|
238
|
+
# This will:
|
239
|
+
# - Set Website to NULL in Salesforce (because it's nil)
|
240
|
+
# - Leave Phone unchanged (because it's in no_null_list)
|
241
|
+
# - Update Name normally
|
242
|
+
```
|
243
|
+
|
244
|
+
### Job Management
|
245
|
+
|
246
|
+
#### Get Job by ID
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
job = salesforce.job_from_id('750A0000001234567')
|
250
|
+
status = job.check_job_status
|
251
|
+
puts "Job state: #{status['state'][0]}"
|
252
|
+
puts "Batches total: #{status['numberBatchesTotal'][0]}"
|
253
|
+
```
|
254
|
+
|
255
|
+
#### Check Job Status
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
job = salesforce.job_from_id(job_id)
|
259
|
+
status = job.check_job_status
|
260
|
+
|
261
|
+
puts "Job Information:"
|
262
|
+
puts " State: #{status['state'][0]}"
|
263
|
+
puts " Object: #{status['object'][0]}"
|
264
|
+
puts " Operation: #{status['operation'][0]}"
|
265
|
+
puts " Total Batches: #{status['numberBatchesTotal'][0]}"
|
266
|
+
puts " Completed Batches: #{status['numberBatchesCompleted'][0]}"
|
267
|
+
puts " Failed Batches: #{status['numberBatchesFailed'][0]}"
|
268
|
+
```
|
269
|
+
|
270
|
+
### Batch Operations
|
271
|
+
|
272
|
+
#### Check Batch Status
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
job = salesforce.job_from_id(job_id)
|
276
|
+
batch_status = job.check_batch_status(batch_id)
|
277
|
+
|
278
|
+
puts "Batch Information:"
|
279
|
+
puts " State: #{batch_status['state'][0]}"
|
280
|
+
puts " Records Processed: #{batch_status['numberRecordsProcessed'][0]}"
|
281
|
+
puts " Records Failed: #{batch_status['numberRecordsFailed'][0]}"
|
282
|
+
```
|
283
|
+
|
284
|
+
#### Retrieve Batch Records
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
job = salesforce.job_from_id(job_id)
|
288
|
+
records = job.get_batch_records(batch_id)
|
289
|
+
|
290
|
+
puts "Batch Records:"
|
291
|
+
records.each do |record|
|
292
|
+
puts " #{record.inspect}"
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
#### Get Batch Results
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
job = salesforce.job_from_id(job_id)
|
300
|
+
results = job.get_batch_result(batch_id)
|
301
|
+
|
302
|
+
results.each do |result|
|
303
|
+
if result["success"] == ["true"]
|
304
|
+
puts "Success: Record ID #{result['id'][0]}"
|
305
|
+
else
|
306
|
+
puts "Failed: #{result['errors'][0]['message'][0]}"
|
307
|
+
end
|
308
|
+
end
|
309
|
+
```
|
310
|
+
|
311
|
+
### Event Listening
|
312
|
+
|
313
|
+
Listen for job creation events:
|
109
314
|
|
110
315
|
```ruby
|
111
|
-
# A job is created
|
112
|
-
# Useful when you need to store the job_id before any work begins, then if you fail during a complex load scenario, you can wait for your
|
113
|
-
# previous job(s) to finish.
|
114
316
|
salesforce.on_job_created do |job|
|
115
|
-
puts "Job #{job.job_id} created!"
|
317
|
+
puts "Job #{job.job_id} created for #{job.operation} on #{job.sobject}!"
|
318
|
+
|
319
|
+
# You can perform additional operations here
|
320
|
+
# like logging, notifications, etc.
|
116
321
|
end
|
322
|
+
|
323
|
+
# Now when you create/update/etc, the listener will be called
|
324
|
+
result = salesforce.create("Account", records)
|
117
325
|
```
|
118
326
|
|
119
|
-
###
|
327
|
+
### API Call Throttling
|
328
|
+
|
329
|
+
Control the frequency of status checks to avoid hitting API limits:
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
# Set status check interval to 30 seconds (default is 5 seconds)
|
333
|
+
salesforce.connection.set_status_throttle(30)
|
334
|
+
|
335
|
+
# Check current throttle setting
|
336
|
+
puts "Current throttle: #{salesforce.connection.get_status_throttle} seconds"
|
337
|
+
```
|
338
|
+
|
339
|
+
### Monitoring and Counters
|
340
|
+
|
341
|
+
Track API usage and operations:
|
120
342
|
|
121
343
|
```ruby
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
# =>
|
126
|
-
|
127
|
-
|
344
|
+
# Get operation counters
|
345
|
+
counters = salesforce.counters
|
346
|
+
puts "API Usage: #{counters}"
|
347
|
+
# => {:http_get=>15, :http_post=>8, :upsert=>2, :update=>1, :create=>3, :delete=>0, :query=>2}
|
348
|
+
|
349
|
+
# Reset counters
|
350
|
+
salesforce.reset_counters
|
351
|
+
```
|
352
|
+
|
353
|
+
## Error Handling
|
128
354
|
|
355
|
+
The gem provides comprehensive error handling:
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
begin
|
359
|
+
result = salesforce.create("Account", records, true)
|
360
|
+
|
361
|
+
# Check for batch-level errors
|
362
|
+
result["batches"].each do |batch|
|
363
|
+
if batch["state"][0] == "Failed"
|
364
|
+
puts "Batch failed: #{batch["stateMessage"][0]}"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
rescue SalesforceBulkApi::Job::SalesforceException => e
|
369
|
+
puts "Salesforce API error: #{e.message}"
|
370
|
+
# Handle API-level errors (invalid objects, fields, etc.)
|
371
|
+
|
372
|
+
rescue SalesforceBulkApi::JobTimeout => e
|
373
|
+
puts "Job timed out: #{e.message}"
|
374
|
+
# Handle timeout errors - job took longer than specified timeout
|
375
|
+
|
376
|
+
rescue => e
|
377
|
+
puts "Unexpected error: #{e.message}"
|
378
|
+
# Handle other errors (network issues, authentication, etc.)
|
379
|
+
end
|
129
380
|
```
|
130
381
|
|
131
|
-
###
|
382
|
+
### Common Error Scenarios
|
132
383
|
|
133
384
|
```ruby
|
134
|
-
#
|
135
|
-
|
136
|
-
|
385
|
+
# Invalid field names
|
386
|
+
begin
|
387
|
+
records = [{ "InvalidField__c" => "value" }]
|
388
|
+
salesforce.create("Account", records, true)
|
389
|
+
rescue SalesforceBulkApi::Job::SalesforceException => e
|
390
|
+
puts "Field error: #{e.message}"
|
391
|
+
end
|
392
|
+
|
393
|
+
# Malformed record IDs
|
394
|
+
begin
|
395
|
+
records = [{ "Id" => "invalid_id" }]
|
396
|
+
salesforce.update("Account", records, true)
|
397
|
+
rescue => e
|
398
|
+
# This might not raise immediately - check batch results
|
399
|
+
result = salesforce.update("Account", records, true)
|
400
|
+
failed_records = result["batches"][0]["response"].select { |r| r["success"] == ["false"] }
|
401
|
+
failed_records.each { |r| puts "Failed: #{r['errors'][0]['message'][0]}" }
|
402
|
+
end
|
403
|
+
```
|
404
|
+
|
405
|
+
## Advanced Features
|
406
|
+
|
407
|
+
### Relationship Fields
|
408
|
+
|
409
|
+
You can work with relationship fields using dot notation:
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
# Create records with relationship data
|
413
|
+
records = [
|
414
|
+
{
|
415
|
+
"Name" => "Test Account",
|
416
|
+
"Parent.Name" => "Parent Account Name",
|
417
|
+
"Owner.Email" => "owner@example.com"
|
418
|
+
}
|
419
|
+
]
|
420
|
+
|
421
|
+
result = salesforce.create("Account", records, true)
|
422
|
+
```
|
423
|
+
|
424
|
+
### Special Data Types
|
425
|
+
|
426
|
+
The gem automatically handles various data types:
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
records = [
|
430
|
+
{
|
431
|
+
"Name" => "Test Account",
|
432
|
+
"AnnualRevenue" => 1000000, # Numbers
|
433
|
+
"IsActive__c" => true, # Booleans
|
434
|
+
"LastModifiedDate" => Time.now, # Timestamps (converted to ISO8601)
|
435
|
+
"Description" => "Text with <special> chars" # XML encoding handled automatically
|
436
|
+
}
|
437
|
+
]
|
438
|
+
```
|
439
|
+
|
440
|
+
### Large Dataset Handling
|
441
|
+
|
442
|
+
For large datasets, the gem automatically handles batching:
|
443
|
+
|
444
|
+
```ruby
|
445
|
+
# This will be automatically split into multiple batches of 10,000 records each
|
446
|
+
large_dataset = (1..50000).map { |i| { "Name" => "Account #{i}" } }
|
447
|
+
|
448
|
+
result = salesforce.create("Account", large_dataset, true, false, [], 10000, 7200) # 2 hour timeout
|
449
|
+
puts "Created #{result['batches'].length} batches"
|
450
|
+
```
|
451
|
+
|
452
|
+
### Custom Batch Sizes
|
453
|
+
|
454
|
+
Optimize for your use case:
|
455
|
+
|
456
|
+
```ruby
|
457
|
+
# Smaller batches for complex records
|
458
|
+
complex_records = [...]
|
459
|
+
salesforce.create("CustomObject__c", complex_records, true, false, [], 2000)
|
460
|
+
|
461
|
+
# Larger batches for simple records (up to 10,000)
|
462
|
+
simple_records = [...]
|
463
|
+
salesforce.create("Account", simple_records, true, false, [], 10000)
|
464
|
+
```
|
465
|
+
|
466
|
+
## Contributing
|
467
|
+
|
468
|
+
We welcome contributions to improve this gem. Feel free to:
|
469
|
+
|
470
|
+
1. Fork the repository
|
471
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
472
|
+
3. Commit your changes (`git commit -am 'Add some amazing feature'`)
|
473
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
474
|
+
5. Create a new Pull Request
|
475
|
+
|
476
|
+
### Development Setup
|
477
|
+
|
478
|
+
```bash
|
479
|
+
git clone https://github.com/yatish27/salesforce_bulk_api.git
|
480
|
+
cd salesforce_bulk_api
|
481
|
+
bundle install
|
482
|
+
|
483
|
+
# Copy environment template
|
484
|
+
cp .env.sample .env
|
485
|
+
# Edit .env with your Salesforce credentials
|
486
|
+
|
487
|
+
# Run tests
|
488
|
+
bundle exec rspec
|
489
|
+
|
490
|
+
# Run RuboCop
|
491
|
+
bundle exec rubocop
|
137
492
|
```
|
138
493
|
|
139
|
-
##
|
494
|
+
## License
|
140
495
|
|
141
|
-
|
496
|
+
This project is licensed under the MIT License, Copyright (c) 2025 - see the [LICENCE](LICENCE) file for details.
|
data/Rakefile
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
require
|
2
|
-
task :
|
3
|
-
RSpec::Core::RakeTask.new
|
1
|
+
require "rspec/core/rake_task"
|
2
|
+
task default: :spec
|
3
|
+
RSpec::Core::RakeTask.new
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module SalesforceBulkApi::Concerns
|
2
2
|
module Throttling
|
3
|
-
|
4
3
|
def throttles
|
5
4
|
@throttles.dup
|
6
5
|
end
|
@@ -48,13 +47,12 @@ module SalesforceBulkApi::Concerns
|
|
48
47
|
@limits
|
49
48
|
end
|
50
49
|
|
51
|
-
def throttle(details={})
|
50
|
+
def throttle(details = {})
|
52
51
|
(@throttles || []).each do |callback|
|
53
52
|
args = [details]
|
54
53
|
args = args[0..callback.arity]
|
55
54
|
callback.call(*args)
|
56
55
|
end
|
57
56
|
end
|
58
|
-
|
59
57
|
end
|
60
58
|
end
|
@@ -1,20 +1,20 @@
|
|
1
|
-
require
|
1
|
+
require "timeout"
|
2
2
|
|
3
3
|
module SalesforceBulkApi
|
4
4
|
class Connection
|
5
5
|
include Concerns::Throttling
|
6
6
|
|
7
|
-
LOGIN_HOST =
|
7
|
+
LOGIN_HOST = "login.salesforce.com"
|
8
8
|
|
9
9
|
def initialize(api_version, client)
|
10
10
|
@client = client
|
11
11
|
@api_version = api_version
|
12
12
|
@path_prefix = "/services/async/#{@api_version}/"
|
13
13
|
|
14
|
-
login
|
14
|
+
login
|
15
15
|
end
|
16
16
|
|
17
|
-
def login
|
17
|
+
def login
|
18
18
|
client_type = @client.class.to_s
|
19
19
|
case client_type
|
20
20
|
when "Restforce::Data::Client"
|
@@ -24,14 +24,14 @@ module SalesforceBulkApi
|
|
24
24
|
@session_id = @client.oauth_token
|
25
25
|
@server_url = @client.instance_url
|
26
26
|
end
|
27
|
-
@instance = parse_instance
|
27
|
+
@instance = parse_instance
|
28
28
|
@instance_host = "#{@instance}.salesforce.com"
|
29
29
|
end
|
30
30
|
|
31
31
|
def post_xml(host, path, xml, headers)
|
32
|
-
host
|
32
|
+
host ||= @instance_host
|
33
33
|
if host != LOGIN_HOST # Not login, need to add session id to header
|
34
|
-
headers[
|
34
|
+
headers["X-SFDC-Session"] = @session_id
|
35
35
|
path = "#{@path_prefix}#{path}"
|
36
36
|
end
|
37
37
|
i = 0
|
@@ -52,10 +52,10 @@ module SalesforceBulkApi
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def get_request(host, path, headers)
|
55
|
-
host
|
55
|
+
host ||= @instance_host
|
56
56
|
path = "#{@path_prefix}#{path}"
|
57
57
|
if host != LOGIN_HOST # Not login, need to add session id to header
|
58
|
-
headers[
|
58
|
+
headers["X-SFDC-Session"] = @session_id
|
59
59
|
end
|
60
60
|
|
61
61
|
count :get
|
@@ -80,19 +80,17 @@ module SalesforceBulkApi
|
|
80
80
|
private
|
81
81
|
|
82
82
|
def get_counters
|
83
|
-
@counters ||= Hash.new
|
83
|
+
@counters ||= Hash.new { |hash, key| hash[key] = 0 }
|
84
84
|
end
|
85
85
|
|
86
86
|
def count(http_method)
|
87
87
|
get_counters[http_method] += 1
|
88
88
|
end
|
89
89
|
|
90
|
-
def parse_instance
|
91
|
-
@instance = @server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}\./).to_s.gsub("https://","").split(".")[0]
|
90
|
+
def parse_instance
|
91
|
+
@instance = @server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}\./).to_s.gsub("https://", "").split(".")[0]
|
92
92
|
@instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.nil? || @instance.empty?
|
93
93
|
@instance
|
94
94
|
end
|
95
|
-
|
96
95
|
end
|
97
|
-
|
98
96
|
end
|