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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a396ff6ca0368b07ef0d575fed3345ec4c607a1598d4907424e4ad9d8a31f320
4
- data.tar.gz: 951672b7488408a307cfa2f5b839db7e41e5bac355f68dd73f78b2daff640e8c
3
+ metadata.gz: 795266bc08c0155140c8281a50f60e45ebbfada3013b8d70032a7d1ce833097d
4
+ data.tar.gz: b119ea90f9cf8c4f9ef5a5af96c3d0c340a37eb7531605893be9e03738805bb9
5
5
  SHA512:
6
- metadata.gz: de9dea929fb56da1d7c5d8e40e5d17069c0aa83bc124928d49f5e51a9cb0d87c09fc44613472394eeff5573347f9f9c7599623e54d88c65256a7efaaf0211d14
7
- data.tar.gz: 875aa664d88b10f545fe346990619e914051dffc12141fb3e48fbaa7094b362fca4c6390abb007bb4d3be7faf255c21d6c2c08c7d1372b99da9744c0555123bb
6
+ metadata.gz: b5a1d165ab6c908dd13ed49eb15e5928a918e337b011375fac49a226656921e6900058975901d27270cf344053751c2cea5ea3365b435b8243283ebb12dc1eb2
7
+ data.tar.gz: 4be3074ac16caa9de211d34e31677e5e460c94423ee4aff1057318f3112b9f963d6a165f8966c487a31c457a1160f4a610999142217b7f33cc10844c0a2ef189
data/LICENCE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015 Yatish Mehta
3
+ Copyright (c) 2025 Yatish Mehta
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,141 +1,182 @@
1
1
  # Salesforce-Bulk-Api
2
+
2
3
  [![Gem Version](https://badge.fury.io/rb/salesforce_bulk_api.png)](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 ruby wrapper for the Salesforce Bulk API.
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
- ## How to use
23
+ ## Installation
11
24
 
12
- Using this gem is simple and straight forward.
25
+ Add this line to your application's Gemfile:
13
26
 
14
- ### Install
27
+ ```ruby
28
+ gem 'salesforce_bulk_api'
29
+ ```
15
30
 
16
- `gem install salesforce_bulk_api`
31
+ And then execute:
17
32
 
18
- or add it to your Gemfile
33
+ ```
34
+ bundle install
35
+ ```
19
36
 
20
- `gem salesforce_bulk_api`
37
+ Or install it directly:
21
38
 
22
- ### Authenticate
39
+ ```
40
+ gem install salesforce_bulk_api
41
+ ```
23
42
 
24
- You can authenticate with Salesforce using two gems, `databasedotcom` & `restforce`.
43
+ ## Authentication
25
44
 
26
- Please check the documentation of the respective gems to learn how to authenticate with Salesforce
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
- [Databasedotcom](https://github.com/heroku/databasedotcom)
29
- [Restforce](https://github.com/ejholmes/restforce)
47
+ Please refer to the documentation of these gems for detailed authentication options:
30
48
 
31
- You can use username password combo, OmniAuth, Oauth2
32
- You can use as many records possible in the Array. Governor limits are taken care of inside the gem.
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
- :client_id => SFDC_APP_CONFIG["client_id"],
39
- :client_secret => SFDC_APP_CONFIG["client_secret"]
60
+ client_id: SFDC_APP_CONFIG["client_id"],
61
+ client_secret: SFDC_APP_CONFIG["client_secret"]
40
62
  )
41
63
  client.authenticate(
42
- :token => " ",
43
- :instance_url => "http://na1.salesforce.com"
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
- OR
71
+ #### Using Restforce:
50
72
 
51
73
  ```ruby
52
74
  require 'salesforce_bulk_api'
75
+
53
76
  client = Restforce.new(
54
- username: SFDC_APP_CONFIG['SFDC_USERNAME'],
55
- password: SFDC_APP_CONFIG['SFDC_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: SFDC_APP_CONFIG['SFDC_CLIENT_ID'],
58
- client_secret: SFDC_APP_CONFIG['SFDC_CLIENT_SECRET'].to_i,
59
- host: SFDC_APP_CONFIG['SFDC_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
- ### Sample operations:
89
+ ## Usage
90
+
91
+ ### Basic Operations
92
+
93
+ #### Create/Insert Records
67
94
 
68
95
  ```ruby
69
- # Insert/Create
70
- # Add as many fields per record as needed.
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 "result is: #{result.inspect}"
99
+ puts "Result: #{result.inspect}"
100
+ ```
77
101
 
78
- # Update
79
- updated_account = Hash["name" => "Test Account -- Updated", id => "a00A0001009zA2m"] # Nearly identical to an insert, but we need to pass the salesforce id.
80
- records_to_update = Array.new
81
- records_to_update.push(updated_account)
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
- # Upsert
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
- # Delete
91
- deleted_account = Hash["id" => "a00A0001009zA2m"] # We only specify the id of the records to delete
92
- records_to_delete = Array.new
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
- # Query
97
- res = salesforce.query("Account", "select id, name, createddate from Account limit 3") # We just need to pass the sobject name and the query string
126
+ #### Query Records
127
+
128
+ ```ruby
129
+ res = salesforce.query("Account", "SELECT id, name, createddate FROM Account LIMIT 3")
98
130
  ```
99
131
 
100
- ### Helpful methods:
132
+ ### Job Management
133
+
134
+ You can check the status of a job using its ID:
101
135
 
102
136
  ```ruby
103
- # Check status of a job via #job_from_id
104
- job = salesforce.job_from_id('a00A0001009zA2m') # Returns a SalesforceBulkApi::Job instance
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 to events:
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
- ### Fetching records from a batch
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
- ### Throttling API calls:
161
+ ### API Call Throttling
162
+
163
+ You can control how frequently status checks are performed:
132
164
 
133
165
  ```ruby
134
- # By default, this gem (and maybe your app driving it) will query job/batch statuses at an unbounded rate. We
135
- # can fix that, e.g.:
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
- ## Contribute
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
- Feel to fork and send Pull request
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 'rspec/core/rake_task'
2
- task :default => :spec
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 !last_request.nil? && only_if.call(details)
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 > 0
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 ||= Hash.new(0)
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
- args = [details]
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 'timeout'
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 = 'login.salesforce.com'
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 = host || @instance_host
33
- if host != LOGIN_HOST # Not login, need to add session id to header
34
- headers['X-SFDC-Session'] = @session_id
35
- path = "#{@path_prefix}#{path}"
36
- end
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 = host || @instance_host
30
+ host ||= @instance_host
56
31
  path = "#{@path_prefix}#{path}"
57
- if host != LOGIN_HOST # Not login, need to add session id to header
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
- def https(host)
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: get_counters[:get],
76
- post: get_counters[:post]
39
+ get: @counters[:get],
40
+ post: @counters[:post]
77
41
  }
78
42
  end
79
43
 
80
44
  private
81
45
 
82
- def get_counters
83
- @counters ||= Hash.new(0)
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 count(http_method)
87
- get_counters[http_method] += 1
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 parse_instance()
91
- @instance = @server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}\./).to_s.gsub("https://","").split(".")[0]
92
- @instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.nil? || @instance.empty?
93
- @instance
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
- end
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