salesforce_bulk_api 1.1.0 → 1.2.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/LICENCE +1 -1
- data/README.md +110 -69
- data/Rakefile +3 -3
- data/lib/salesforce_bulk_api/concerns/throttling.rb +7 -18
- data/lib/salesforce_bulk_api/connection.rb +56 -62
- data/lib/salesforce_bulk_api/job.rb +84 -205
- data/lib/salesforce_bulk_api/version.rb +1 -1
- data/lib/salesforce_bulk_api.rb +35 -47
- data/salesforce_bulk_api.gemspec +17 -18
- data/spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb +105 -113
- data/spec/spec_helper.rb +4 -4
- metadata +33 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 795266bc08c0155140c8281a50f60e45ebbfada3013b8d70032a7d1ce833097d
|
4
|
+
data.tar.gz: b119ea90f9cf8c4f9ef5a5af96c3d0c340a37eb7531605893be9e03738805bb9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5a1d165ab6c908dd13ed49eb15e5928a918e337b011375fac49a226656921e6900058975901d27270cf344053751c2cea5ea3365b435b8243283ebb12dc1eb2
|
7
|
+
data.tar.gz: 4be3074ac16caa9de211d34e31677e5e460c94423ee4aff1057318f3112b9f963d6a165f8966c487a31c457a1160f4a610999142217b7f33cc10844c0a2ef189
|
data/LICENCE
CHANGED
data/README.md
CHANGED
@@ -1,141 +1,182 @@
|
|
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
|
+
- [Job Management](#job-management)
|
13
|
+
- [Event Listening](#event-listening)
|
14
|
+
- [Retrieving Batch Records](#retrieving-batch-records)
|
15
|
+
- [API Call Throttling](#api-call-throttling)
|
16
|
+
- [Contributing](#contributing)
|
17
|
+
- [License](#license)
|
18
|
+
|
4
19
|
## Overview
|
5
20
|
|
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`.
|
21
|
+
`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
22
|
|
10
|
-
##
|
23
|
+
## Installation
|
11
24
|
|
12
|
-
|
25
|
+
Add this line to your application's Gemfile:
|
13
26
|
|
14
|
-
|
27
|
+
```ruby
|
28
|
+
gem 'salesforce_bulk_api'
|
29
|
+
```
|
15
30
|
|
16
|
-
|
31
|
+
And then execute:
|
17
32
|
|
18
|
-
|
33
|
+
```
|
34
|
+
bundle install
|
35
|
+
```
|
19
36
|
|
20
|
-
|
37
|
+
Or install it directly:
|
21
38
|
|
22
|
-
|
39
|
+
```
|
40
|
+
gem install salesforce_bulk_api
|
41
|
+
```
|
23
42
|
|
24
|
-
|
43
|
+
## Authentication
|
25
44
|
|
26
|
-
|
45
|
+
You can authenticate with Salesforce using either `databasedotcom` or `restforce` gems. Both support various authentication methods including username/password, OmniAuth, and OAuth2.
|
27
46
|
|
28
|
-
|
29
|
-
[Restforce](https://github.com/ejholmes/restforce)
|
47
|
+
Please refer to the documentation of these gems for detailed authentication options:
|
30
48
|
|
31
|
-
|
32
|
-
|
49
|
+
- [Databasedotcom](https://github.com/heroku/databasedotcom)
|
50
|
+
- [Restforce](https://github.com/ejholmes/restforce)
|
51
|
+
|
52
|
+
### Authentication Examples
|
53
|
+
|
54
|
+
#### Using Databasedotcom:
|
33
55
|
|
34
56
|
```ruby
|
35
57
|
require 'salesforce_bulk_api'
|
36
58
|
|
37
59
|
client = Databasedotcom::Client.new(
|
38
|
-
:
|
39
|
-
:
|
60
|
+
client_id: SFDC_APP_CONFIG["client_id"],
|
61
|
+
client_secret: SFDC_APP_CONFIG["client_secret"]
|
40
62
|
)
|
41
63
|
client.authenticate(
|
42
|
-
:
|
43
|
-
:
|
64
|
+
token: " ",
|
65
|
+
instance_url: "http://na1.salesforce.com"
|
44
66
|
)
|
45
67
|
|
46
68
|
salesforce = SalesforceBulkApi::Api.new(client)
|
47
69
|
```
|
48
70
|
|
49
|
-
|
71
|
+
#### Using Restforce:
|
50
72
|
|
51
73
|
```ruby
|
52
74
|
require 'salesforce_bulk_api'
|
75
|
+
|
53
76
|
client = Restforce.new(
|
54
|
-
username:
|
55
|
-
password:
|
77
|
+
username: SFDC_APP_CONFIG['SFDC_USERNAME'],
|
78
|
+
password: SFDC_APP_CONFIG['SFDC_PASSWORD'],
|
56
79
|
security_token: SFDC_APP_CONFIG['SFDC_SECURITY_TOKEN'],
|
57
|
-
client_id:
|
58
|
-
client_secret:
|
59
|
-
host:
|
80
|
+
client_id: SFDC_APP_CONFIG['SFDC_CLIENT_ID'],
|
81
|
+
client_secret: SFDC_APP_CONFIG['SFDC_CLIENT_SECRET'],
|
82
|
+
host: SFDC_APP_CONFIG['SFDC_HOST']
|
60
83
|
)
|
61
84
|
client.authenticate!
|
62
85
|
|
63
86
|
salesforce = SalesforceBulkApi::Api.new(client)
|
64
87
|
```
|
65
88
|
|
66
|
-
|
89
|
+
## Usage
|
90
|
+
|
91
|
+
### Basic Operations
|
92
|
+
|
93
|
+
#### Create/Insert Records
|
67
94
|
|
68
95
|
```ruby
|
69
|
-
|
70
|
-
|
71
|
-
new_account = Hash["name" => "Test Account", "type" => "Other"]
|
72
|
-
records_to_insert = Array.new
|
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)
|
96
|
+
new_account = { "name" => "Test Account", "type" => "Other" }
|
97
|
+
records_to_insert = [new_account]
|
75
98
|
result = salesforce.create("Account", records_to_insert)
|
76
|
-
puts "
|
99
|
+
puts "Result: #{result.inspect}"
|
100
|
+
```
|
77
101
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
102
|
+
#### Update Records
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
updated_account = { "name" => "Test Account -- Updated", "id" => "a00A0001009zA2m" }
|
106
|
+
records_to_update = [updated_account]
|
82
107
|
salesforce.update("Account", records_to_update)
|
108
|
+
```
|
109
|
+
|
110
|
+
#### Upsert Records
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
upserted_account = { "name" => "Test Account -- Upserted", "External_Field_Name" => "123456" }
|
114
|
+
records_to_upsert = [upserted_account]
|
115
|
+
salesforce.upsert("Account", records_to_upsert, "External_Field_Name")
|
116
|
+
```
|
83
117
|
|
84
|
-
|
85
|
-
upserted_account = Hash["name" => "Test Account -- Upserted", "External_Field_Name" => "123456"] # Fields to be updated. External field must be included
|
86
|
-
records_to_upsert = Array.new
|
87
|
-
records_to_upsert.push(upserted_account)
|
88
|
-
salesforce.upsert("Account", records_to_upsert, "External_Field_Name") # Note that upsert accepts an extra parameter for the external field name
|
118
|
+
#### Delete Records
|
89
119
|
|
90
|
-
|
91
|
-
deleted_account =
|
92
|
-
records_to_delete =
|
93
|
-
records_to_delete.push(deleted_account)
|
120
|
+
```ruby
|
121
|
+
deleted_account = { "id" => "a00A0001009zA2m" }
|
122
|
+
records_to_delete = [deleted_account]
|
94
123
|
salesforce.delete("Account", records_to_delete)
|
124
|
+
```
|
95
125
|
|
96
|
-
|
97
|
-
|
126
|
+
#### Query Records
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
res = salesforce.query("Account", "SELECT id, name, createddate FROM Account LIMIT 3")
|
98
130
|
```
|
99
131
|
|
100
|
-
###
|
132
|
+
### Job Management
|
133
|
+
|
134
|
+
You can check the status of a job using its ID:
|
101
135
|
|
102
136
|
```ruby
|
103
|
-
|
104
|
-
|
105
|
-
puts "status is: #{job.check_job_status.inspect}"
|
137
|
+
job = salesforce.job_from_id('a00A0001009zA2m')
|
138
|
+
puts "Status: #{job.check_job_status.inspect}"
|
106
139
|
```
|
107
140
|
|
108
|
-
### Listening
|
141
|
+
### Event Listening
|
142
|
+
|
143
|
+
You can listen for job creation events:
|
109
144
|
|
110
145
|
```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
146
|
salesforce.on_job_created do |job|
|
115
147
|
puts "Job #{job.job_id} created!"
|
116
148
|
end
|
117
149
|
```
|
118
150
|
|
119
|
-
###
|
151
|
+
### Retrieving Batch Records
|
152
|
+
|
153
|
+
Fetch records from a specific batch in a job:
|
120
154
|
|
121
155
|
```ruby
|
122
156
|
job_id = 'l02A0231009Za8m'
|
123
157
|
batch_id = 'H24a0708089zA2J'
|
124
|
-
salesforce.get_batch_records(job_id, batch_id)
|
125
|
-
# => [{"Id"=>["RECORD_ID_1"], "AField__c"=>["123123"]},
|
126
|
-
{"Id"=>["RECORD_ID_2"], "AField__c"=>["123123"]},
|
127
|
-
{"Id"=>["RECORD_ID_3"], "AField__c"=>["123123"]}]
|
128
|
-
|
158
|
+
records = salesforce.get_batch_records(job_id, batch_id)
|
129
159
|
```
|
130
160
|
|
131
|
-
###
|
161
|
+
### API Call Throttling
|
162
|
+
|
163
|
+
You can control how frequently status checks are performed:
|
132
164
|
|
133
165
|
```ruby
|
134
|
-
#
|
135
|
-
|
136
|
-
salesforce.connection.set_status_throttle(30) # only check status of individual jobs/batches every 30 seconds
|
166
|
+
# Set status check interval to 30 seconds
|
167
|
+
salesforce.connection.set_status_throttle(30)
|
137
168
|
```
|
138
169
|
|
139
|
-
##
|
170
|
+
## Contributing
|
171
|
+
|
172
|
+
We welcome contributions to improve this gem. Feel free to:
|
173
|
+
|
174
|
+
1. Fork the repository
|
175
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
176
|
+
3. Commit your changes (`git commit -am 'Add some amazing feature'`)
|
177
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
178
|
+
5. Create a new Pull Request
|
179
|
+
|
180
|
+
## License
|
140
181
|
|
141
|
-
|
182
|
+
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
|
@@ -20,10 +19,10 @@ module SalesforceBulkApi::Concerns
|
|
20
19
|
key = extract_constraint_key_from(details, throttle_by_keys)
|
21
20
|
last_request = limit_log[key]
|
22
21
|
|
23
|
-
if
|
22
|
+
if last_request && only_if.call(details)
|
24
23
|
seconds_since_last_request = Time.now.to_f - last_request.to_f
|
25
24
|
need_to_wait_seconds = limit_seconds - seconds_since_last_request
|
26
|
-
sleep(need_to_wait_seconds) if need_to_wait_seconds
|
25
|
+
sleep(need_to_wait_seconds) if need_to_wait_seconds.positive?
|
27
26
|
end
|
28
27
|
|
29
28
|
limit_log[key] = Time.now
|
@@ -33,28 +32,18 @@ module SalesforceBulkApi::Concerns
|
|
33
32
|
private
|
34
33
|
|
35
34
|
def extract_constraint_key_from(details, throttle_by_keys)
|
36
|
-
hash =
|
37
|
-
throttle_by_keys.each { |k| hash[k] = details[k] }
|
38
|
-
hash
|
35
|
+
throttle_by_keys.each_with_object({}) { |k, hash| hash[k] = details[k] }
|
39
36
|
end
|
40
37
|
|
41
38
|
def get_limit_log(prune_older_than)
|
42
|
-
@limits ||=
|
43
|
-
|
44
|
-
@limits.delete_if do |k, v|
|
45
|
-
v < prune_older_than
|
46
|
-
end
|
47
|
-
|
48
|
-
@limits
|
39
|
+
@limits ||= {}
|
40
|
+
@limits.delete_if { |_, v| v < prune_older_than }
|
49
41
|
end
|
50
42
|
|
51
|
-
def throttle(details={})
|
43
|
+
def throttle(details = {})
|
52
44
|
(@throttles || []).each do |callback|
|
53
|
-
|
54
|
-
args = args[0..callback.arity]
|
55
|
-
callback.call(*args)
|
45
|
+
callback.call(details)
|
56
46
|
end
|
57
47
|
end
|
58
|
-
|
59
48
|
end
|
60
49
|
end
|
@@ -1,98 +1,92 @@
|
|
1
|
-
require
|
1
|
+
require "timeout"
|
2
|
+
require "net/https"
|
2
3
|
|
3
4
|
module SalesforceBulkApi
|
4
5
|
class Connection
|
5
6
|
include Concerns::Throttling
|
6
7
|
|
7
|
-
LOGIN_HOST =
|
8
|
+
LOGIN_HOST = "login.salesforce.com".freeze
|
9
|
+
|
10
|
+
attr_reader :session_id, :server_url, :instance, :instance_host
|
8
11
|
|
9
12
|
def initialize(api_version, client)
|
10
13
|
@client = client
|
11
14
|
@api_version = api_version
|
12
15
|
@path_prefix = "/services/async/#{@api_version}/"
|
16
|
+
@counters = Hash.new(0)
|
13
17
|
|
14
|
-
login
|
15
|
-
end
|
16
|
-
|
17
|
-
def login()
|
18
|
-
client_type = @client.class.to_s
|
19
|
-
case client_type
|
20
|
-
when "Restforce::Data::Client"
|
21
|
-
@session_id = @client.options[:oauth_token]
|
22
|
-
@server_url = @client.options[:instance_url]
|
23
|
-
else
|
24
|
-
@session_id = @client.oauth_token
|
25
|
-
@server_url = @client.instance_url
|
26
|
-
end
|
27
|
-
@instance = parse_instance()
|
28
|
-
@instance_host = "#{@instance}.salesforce.com"
|
18
|
+
login
|
29
19
|
end
|
30
20
|
|
31
21
|
def post_xml(host, path, xml, headers)
|
32
|
-
host
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
i = 0
|
38
|
-
begin
|
39
|
-
count :post
|
40
|
-
throttle(http_method: :post, path: path)
|
41
|
-
https(host).post(path, xml, headers).body
|
42
|
-
rescue
|
43
|
-
i += 1
|
44
|
-
if i < 3
|
45
|
-
puts "Request fail #{i}: Retrying #{path}"
|
46
|
-
retry
|
47
|
-
else
|
48
|
-
puts "FATAL: Request to #{path} failed three times."
|
49
|
-
raise
|
50
|
-
end
|
51
|
-
end
|
22
|
+
host ||= @instance_host
|
23
|
+
headers["X-SFDC-Session"] = @session_id unless host == LOGIN_HOST
|
24
|
+
path = "#{@path_prefix}#{path}" unless host == LOGIN_HOST
|
25
|
+
|
26
|
+
perform_request(:post, host, path, xml, headers)
|
52
27
|
end
|
53
28
|
|
54
29
|
def get_request(host, path, headers)
|
55
|
-
host
|
30
|
+
host ||= @instance_host
|
56
31
|
path = "#{@path_prefix}#{path}"
|
57
|
-
|
58
|
-
headers['X-SFDC-Session'] = @session_id;
|
59
|
-
end
|
60
|
-
|
61
|
-
count :get
|
62
|
-
throttle(http_method: :get, path: path)
|
63
|
-
https(host).get(path, headers).body
|
64
|
-
end
|
32
|
+
headers["X-SFDC-Session"] = @session_id unless host == LOGIN_HOST
|
65
33
|
|
66
|
-
|
67
|
-
req = Net::HTTP.new(host, 443)
|
68
|
-
req.use_ssl = true
|
69
|
-
req.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
70
|
-
req
|
34
|
+
perform_request(:get, host, path, nil, headers)
|
71
35
|
end
|
72
36
|
|
73
37
|
def counters
|
74
38
|
{
|
75
|
-
get:
|
76
|
-
post:
|
39
|
+
get: @counters[:get],
|
40
|
+
post: @counters[:post]
|
77
41
|
}
|
78
42
|
end
|
79
43
|
|
80
44
|
private
|
81
45
|
|
82
|
-
def
|
83
|
-
|
46
|
+
def login
|
47
|
+
client_type = @client.class.to_s
|
48
|
+
@session_id, @server_url = if client_type == "Restforce::Data::Client"
|
49
|
+
[@client.options[:oauth_token], @client.options[:instance_url]]
|
50
|
+
else
|
51
|
+
[@client.oauth_token, @client.instance_url]
|
52
|
+
end
|
53
|
+
@instance = parse_instance
|
54
|
+
@instance_host = "#{@instance}.salesforce.com"
|
84
55
|
end
|
85
56
|
|
86
|
-
def
|
87
|
-
|
57
|
+
def perform_request(method, host, path, body, headers)
|
58
|
+
retries = 0
|
59
|
+
begin
|
60
|
+
count(method)
|
61
|
+
throttle(http_method: method, path: path)
|
62
|
+
response = https(host).public_send(method, path, body, headers)
|
63
|
+
response.body
|
64
|
+
rescue => e
|
65
|
+
retries += 1
|
66
|
+
if retries < 3
|
67
|
+
puts "Request fail #{retries}: Retrying #{path}"
|
68
|
+
retry
|
69
|
+
else
|
70
|
+
puts "FATAL: Request to #{path} failed three times."
|
71
|
+
raise e
|
72
|
+
end
|
73
|
+
end
|
88
74
|
end
|
89
75
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
76
|
+
def https(host)
|
77
|
+
Net::HTTP.new(host, 443).tap do |http|
|
78
|
+
http.use_ssl = true
|
79
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
80
|
+
end
|
94
81
|
end
|
95
82
|
|
96
|
-
|
83
|
+
def count(http_method)
|
84
|
+
@counters[http_method] += 1
|
85
|
+
end
|
97
86
|
|
87
|
+
def parse_instance
|
88
|
+
instance = @server_url.match(%r{https://([a-z]{2}[0-9]{1,2})\.})&.captures&.first
|
89
|
+
instance || @server_url.split(".salesforce.com").first.split("://").last
|
90
|
+
end
|
91
|
+
end
|
98
92
|
end
|