salesforce_bulk_api_serial_or_parallel 0.1
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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENCE +21 -0
- data/README.md +112 -0
- data/Rakefile +3 -0
- data/auth_credentials.yml.example +7 -0
- data/lib/salesforce_bulk_api_serial_or_parallel.rb +98 -0
- data/lib/salesforce_bulk_api_serial_or_parallel/concerns/throttling.rb +60 -0
- data/lib/salesforce_bulk_api_serial_or_parallel/connection.rb +98 -0
- data/lib/salesforce_bulk_api_serial_or_parallel/job.rb +235 -0
- data/lib/salesforce_bulk_api_serial_or_parallel/version.rb +3 -0
- data/salesforce_bulk_api_serial_or_parallel.gemspec +30 -0
- data/spec/salesforce_bulk_api_serial_or_parallel/salesforce_bulk_api_serial_or_parallel.rb +193 -0
- data/spec/spec_helper.rb +8 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b42acc44b480f4bc913f265ca0e5f1ee9d6eb27c
|
4
|
+
data.tar.gz: 2fae9dadedce4fea9d26be4828a27c0cbc662304
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: de7e1e445ecacd7b832830175fa501146de6e210f1b653a3f579f129135993b12a592802716ab3d523a214031f99643928f202bbe35d73d7deca0a7cac3ef32f
|
7
|
+
data.tar.gz: 9545be081a46a0f17a1e34ff20c8cf30fcfe9277dded5b21f7cc460fd0431c55edde2f74e751a145a0bec57e73fec52ebd9e6a1e8e9110bcff6ea6f3469eee49
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENCE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Yatish Mehta
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# Salesforce-Bulk-Api
|
2
|
+
[](http://badge.fury.io/rb/salesforce_bulk_api)
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Salesforce bulk API is a simple ruby gem for connecting to and using the Salesforce Bulk API. It is actually a re-written code from [salesforce_bulk](https://github.com/jorgevaldivia/salesforce_bulk).Written to suit many more other features as well.
|
6
|
+
|
7
|
+
## How to use
|
8
|
+
|
9
|
+
Using this gem is simple and straight forward.
|
10
|
+
|
11
|
+
To initialize:
|
12
|
+
|
13
|
+
`sudo gem install salesforce_bulk_api_serial_or_parallel`
|
14
|
+
|
15
|
+
or add
|
16
|
+
|
17
|
+
`gem salesforce_bulk_api_serial_or_parallel`
|
18
|
+
|
19
|
+
in your Gemfile
|
20
|
+
|
21
|
+
There are two ways to authenticate with SalesForce to use the Bulk API: databasedotcom & restforce.
|
22
|
+
Please check out the entire documentation of the gem you decide to use to learn the various ways of authentication.
|
23
|
+
|
24
|
+
[Databasedotcom](https://github.com/heroku/databasedotcom)
|
25
|
+
[Restforce](https://github.com/ejholmes/restforce)
|
26
|
+
|
27
|
+
|
28
|
+
You can use username password combo, OmniAuth, Oauth2
|
29
|
+
You can use as many records possible in the Array. Governor limits are taken care of inside the gem.
|
30
|
+
|
31
|
+
|
32
|
+
require 'salesforce_bulk_api_serial_or_parallel'
|
33
|
+
client = Databasedotcom::Client.new :client_id => SFDC_APP_CONFIG["client_id"], :client_secret => SFDC_APP_CONFIG["client_secret"] #client_id and client_secret respectively
|
34
|
+
client.authenticate :token => "my-oauth-token", :instance_url => "http://na1.salesforce.com" #=> "my-oauth-token"
|
35
|
+
|
36
|
+
salesforce = SalesforceBulkApi::Api.new(client)
|
37
|
+
|
38
|
+
OR
|
39
|
+
|
40
|
+
require 'salesforce_bulk_api_serial_or_parallel'
|
41
|
+
client = Restforce.new(
|
42
|
+
username: SFDC_APP_CONFIG['SFDC_USERNAME'],
|
43
|
+
password: SFDC_APP_CONFIG['SFDC_PASSWORD'],
|
44
|
+
security_token: SFDC_APP_CONFIG['SFDC_SECURITY_TOKEN'],
|
45
|
+
client_id: SFDC_APP_CONFIG['SFDC_CLIENT_ID'],
|
46
|
+
client_secret: SFDC_APP_CONFIG['SFDC_CLIENT_SECRET'].to_i,
|
47
|
+
host: SFDC_APP_CONFIG['SFDC_HOST']
|
48
|
+
)
|
49
|
+
client.authenticate!
|
50
|
+
salesforce = SalesforceBulkApi::Api.new(client)
|
51
|
+
|
52
|
+
|
53
|
+
### Sample operations:
|
54
|
+
|
55
|
+
# Insert/Create
|
56
|
+
# Add as many fields per record as needed.
|
57
|
+
new_account = Hash["name" => "Test Account", "type" => "Other"]
|
58
|
+
records_to_insert = Array.new
|
59
|
+
# You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
|
60
|
+
records_to_insert.push(new_account)
|
61
|
+
result = salesforce.create("Account", records_to_insert)
|
62
|
+
puts "result is: #{result.inspect}"
|
63
|
+
|
64
|
+
# Update
|
65
|
+
updated_account = Hash["name" => "Test Account -- Updated", id => "a00A0001009zA2m"] # Nearly identical to an insert, but we need to pass the salesforce id.
|
66
|
+
records_to_update = Array.new
|
67
|
+
records_to_update.push(updated_account)
|
68
|
+
salesforce.update("Account", records_to_update)
|
69
|
+
|
70
|
+
# Upsert
|
71
|
+
upserted_account = Hash["name" => "Test Account -- Upserted", "External_Field_Name" => "123456"] # Fields to be updated. External field must be included
|
72
|
+
records_to_upsert = Array.new
|
73
|
+
records_to_upsert.push(upserted_account)
|
74
|
+
salesforce.upsert("Account", records_to_upsert, "External_Field_Name") # Note that upsert accepts an extra parameter for the external field name
|
75
|
+
|
76
|
+
# Delete
|
77
|
+
deleted_account = Hash["id" => "a00A0001009zA2m"] # We only specify the id of the records to delete
|
78
|
+
records_to_delete = Array.new
|
79
|
+
records_to_delete.push(deleted_account)
|
80
|
+
salesforce.delete("Account", records_to_delete)
|
81
|
+
|
82
|
+
# Query
|
83
|
+
res = salesforce.query("Account", "select id, name, createddate from Account limit 3") # We just need to pass the sobject name and the query string
|
84
|
+
|
85
|
+
### Helpful methods:
|
86
|
+
|
87
|
+
# Check status of a job via #job_from_id
|
88
|
+
job = salesforce.job_from_id('a00A0001009zA2m') # Returns a SalesforceBulkApi::Job instance
|
89
|
+
puts "status is: #{job.check_job_status.inspect}"
|
90
|
+
|
91
|
+
### Listening to events:
|
92
|
+
|
93
|
+
# A job is created
|
94
|
+
# 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
|
95
|
+
# previous job(s) to finish.
|
96
|
+
salesforce.on_job_created do |job|
|
97
|
+
puts "Job #{job.job_id} created!"
|
98
|
+
end
|
99
|
+
|
100
|
+
### Throttling API calls:
|
101
|
+
|
102
|
+
# By default, this gem (and maybe your app driving it) will query job/batch statuses at an unbounded rate. We
|
103
|
+
# can fix that, e.g.:
|
104
|
+
salesforce.connection.set_status_throttle(30) # only check status of individual jobs/batches every 30 seconds
|
105
|
+
|
106
|
+
## Installation
|
107
|
+
|
108
|
+
sudo gem install salesforce_bulk_api_serial_or_parallel
|
109
|
+
|
110
|
+
## Contribute
|
111
|
+
|
112
|
+
Feel to fork and send Pull request
|
data/Rakefile
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'net/https'
|
4
|
+
require 'xmlsimple'
|
5
|
+
require 'csv'
|
6
|
+
|
7
|
+
require 'salesforce_bulk_api_serial_or_parallel/version'
|
8
|
+
require 'salesforce_bulk_api_serial_or_parallel/concerns/throttling'
|
9
|
+
require 'salesforce_bulk_api_serial_or_parallel/job'
|
10
|
+
require 'salesforce_bulk_api_serial_or_parallel/connection'
|
11
|
+
|
12
|
+
module SalesforceBulkApiSerialOrParallel
|
13
|
+
class Api
|
14
|
+
attr_reader :connection
|
15
|
+
|
16
|
+
SALESFORCE_API_VERSION = '1.0.1'
|
17
|
+
|
18
|
+
def initialize(client)
|
19
|
+
@connection = SalesforceBulkApiSerialOrParallel::Connection.new(SALESFORCE_API_VERSION, client)
|
20
|
+
@listeners = { job_created: [] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def upsert(sobject, records, external_field, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500, concurrency = nil)
|
24
|
+
do_operation('upsert', sobject, records, external_field, get_response, timeout, batch_size, send_nulls, no_null_list, concurrency)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(sobject, records, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500, concurrency = nil)
|
28
|
+
do_operation('update', sobject, records, nil, get_response, timeout, batch_size, send_nulls, no_null_list, concurrency)
|
29
|
+
end
|
30
|
+
|
31
|
+
def create(sobject, records, get_response = false, send_nulls = false, batch_size = 10000, timeout = 1500, concurrency = nil)
|
32
|
+
do_operation('insert', sobject, records, nil, get_response, timeout, batch_size, send_nulls, concurrency)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(sobject, records, get_response = false, batch_size = 10000, timeout = 1500)
|
36
|
+
do_operation('delete', sobject, records, nil, get_response, timeout, batch_size, concurrency)
|
37
|
+
end
|
38
|
+
|
39
|
+
def query(sobject, query, batch_size = 10000, timeout = 1500)
|
40
|
+
do_operation('query', sobject, query, nil, true, timeout, batch_size)
|
41
|
+
end
|
42
|
+
|
43
|
+
def counters
|
44
|
+
{
|
45
|
+
http_get: @connection.counters[:get],
|
46
|
+
http_post: @connection.counters[:post],
|
47
|
+
upsert: get_counters[:upsert],
|
48
|
+
update: get_counters[:update],
|
49
|
+
create: get_counters[:create],
|
50
|
+
delete: get_counters[:delete],
|
51
|
+
query: get_counters[:query]
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
##
|
57
|
+
# Allows you to attach a listener that accepts the created job (which has a useful #job_id field). This is useful
|
58
|
+
# for recording a job ID persistently before you begin batch work (i.e. start modifying the salesforce database),
|
59
|
+
# so if the load process you are writing needs to recover, it can be aware of previous jobs it started and wait
|
60
|
+
# for them to finish.
|
61
|
+
def on_job_created(&block)
|
62
|
+
@listeners[:job_created] << block
|
63
|
+
end
|
64
|
+
|
65
|
+
def job_from_id(job_id)
|
66
|
+
SalesforceBulkApiSerialOrParallel::Job.new(job_id: job_id, connection: @connection)
|
67
|
+
end
|
68
|
+
|
69
|
+
def do_operation(operation, sobject, records, external_field, get_response, timeout, batch_size, send_nulls = false, no_null_list = [], concurrency_mode = nil)
|
70
|
+
count operation.to_sym
|
71
|
+
|
72
|
+
job = SalesforceBulkApiSerialOrParallel::Job.new(
|
73
|
+
operation: operation,
|
74
|
+
sobject: sobject,
|
75
|
+
records: records,
|
76
|
+
external_field: external_field,
|
77
|
+
connection: @connection
|
78
|
+
)
|
79
|
+
|
80
|
+
job.create_job(batch_size, send_nulls, no_null_list, concurrency_mode)
|
81
|
+
@listeners[:job_created].each {|callback| callback.call(job)}
|
82
|
+
operation == "query" ? job.add_query() : job.add_batches()
|
83
|
+
response = job.close_job
|
84
|
+
response.merge!({'batches' => job.get_job_result(get_response, timeout)}) if get_response == true
|
85
|
+
response
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def get_counters
|
90
|
+
@counters ||= Hash.new(0)
|
91
|
+
end
|
92
|
+
|
93
|
+
def count(name)
|
94
|
+
get_counters[name] += 1
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module SalesforceBulkApiSerialOrParallel::Concerns
|
2
|
+
module Throttling
|
3
|
+
|
4
|
+
def throttles
|
5
|
+
@throttles.dup
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_throttle(&throttling_callback)
|
9
|
+
@throttles ||= []
|
10
|
+
@throttles << throttling_callback
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_status_throttle(limit_seconds)
|
14
|
+
set_throttle_limit_in_seconds(limit_seconds, [:http_method, :path], ->(details) { details[:http_method] == :get })
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_throttle_limit_in_seconds(limit_seconds, throttle_by_keys, only_if)
|
18
|
+
add_throttle do |details|
|
19
|
+
limit_log = get_limit_log(Time.now - limit_seconds)
|
20
|
+
key = extract_constraint_key_from(details, throttle_by_keys)
|
21
|
+
last_request = limit_log[key]
|
22
|
+
|
23
|
+
if !last_request.nil? && only_if.call(details)
|
24
|
+
seconds_since_last_request = Time.now.to_f - last_request.to_f
|
25
|
+
need_to_wait_seconds = limit_seconds - seconds_since_last_request
|
26
|
+
sleep(need_to_wait_seconds) if need_to_wait_seconds > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
limit_log[key] = Time.now
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def extract_constraint_key_from(details, throttle_by_keys)
|
36
|
+
hash = {}
|
37
|
+
throttle_by_keys.each { |k| hash[k] = details[k] }
|
38
|
+
hash
|
39
|
+
end
|
40
|
+
|
41
|
+
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
|
49
|
+
end
|
50
|
+
|
51
|
+
def throttle(details={})
|
52
|
+
(@throttles || []).each do |callback|
|
53
|
+
args = [details]
|
54
|
+
args = args[0..callback.arity]
|
55
|
+
callback.call(*args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module SalesforceBulkApiSerialOrParallel
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
class Connection
|
5
|
+
include Concerns::Throttling
|
6
|
+
|
7
|
+
LOGIN_HOST = 'login.salesforce.com'
|
8
|
+
|
9
|
+
def initialize(api_version, client)
|
10
|
+
@client = client
|
11
|
+
@api_version = api_version
|
12
|
+
@path_prefix = "/services/async/#{@api_version}/"
|
13
|
+
|
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"
|
29
|
+
end
|
30
|
+
|
31
|
+
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
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_request(host, path, headers)
|
55
|
+
host = host || @instance_host
|
56
|
+
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
|
65
|
+
|
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
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_instance()
|
74
|
+
@instance = @server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}/).to_s.gsub("https://","")
|
75
|
+
@instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.nil? || @instance.empty?
|
76
|
+
return @instance
|
77
|
+
end
|
78
|
+
|
79
|
+
def counters
|
80
|
+
{
|
81
|
+
get: get_counters[:get],
|
82
|
+
post: get_counters[:post]
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def get_counters
|
89
|
+
@counters ||= Hash.new(0)
|
90
|
+
end
|
91
|
+
|
92
|
+
def count(http_method)
|
93
|
+
get_counters[http_method] += 1
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
module SalesforceBulkApiSerialOrParallel
|
2
|
+
|
3
|
+
class Job
|
4
|
+
attr_reader :job_id
|
5
|
+
|
6
|
+
class SalesforceException < StandardError; end
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@job_id = args[:job_id]
|
10
|
+
@operation = args[:operation]
|
11
|
+
@sobject = args[:sobject]
|
12
|
+
@external_field = args[:external_field]
|
13
|
+
@records = args[:records]
|
14
|
+
@connection = args[:connection]
|
15
|
+
@batch_ids = []
|
16
|
+
@XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>'
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_job(batch_size, send_nulls, no_null_list, concurrency_mode)
|
20
|
+
@batch_size = batch_size
|
21
|
+
@send_nulls = send_nulls
|
22
|
+
@no_null_list = no_null_list
|
23
|
+
@concurrency_mode = concurrency_mode
|
24
|
+
|
25
|
+
xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
|
26
|
+
xml += "<operation>#{@operation}</operation>"
|
27
|
+
xml += "<object>#{@sobject}</object>"
|
28
|
+
if @concurrency_mode
|
29
|
+
xml += "<concurrencyMode>#{@concurrency_mode}</concurrencyMode>"
|
30
|
+
end
|
31
|
+
# This only happens on upsert
|
32
|
+
if !@external_field.nil?
|
33
|
+
xml += "<externalIdFieldName>#{@external_field}</externalIdFieldName>"
|
34
|
+
end
|
35
|
+
xml += "<contentType>XML</contentType>"
|
36
|
+
xml += "</jobInfo>"
|
37
|
+
|
38
|
+
path = "job"
|
39
|
+
headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
|
40
|
+
|
41
|
+
response = @connection.post_xml(nil, path, xml, headers)
|
42
|
+
response_parsed = XmlSimple.xml_in(response)
|
43
|
+
|
44
|
+
# response may contain an exception, so raise it
|
45
|
+
raise SalesforceException.new("#{response_parsed['exceptionMessage'][0]} (#{response_parsed['exceptionCode'][0]})") if response_parsed['exceptionCode']
|
46
|
+
|
47
|
+
@job_id = response_parsed['id'][0]
|
48
|
+
end
|
49
|
+
|
50
|
+
def close_job()
|
51
|
+
xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
|
52
|
+
xml += "<state>Closed</state>"
|
53
|
+
xml += "</jobInfo>"
|
54
|
+
|
55
|
+
path = "job/#{@job_id}"
|
56
|
+
headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
|
57
|
+
|
58
|
+
response = @connection.post_xml(nil, path, xml, headers)
|
59
|
+
XmlSimple.xml_in(response)
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_query
|
63
|
+
path = "job/#{@job_id}/batch/"
|
64
|
+
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
65
|
+
|
66
|
+
response = @connection.post_xml(nil, path, @records, headers)
|
67
|
+
response_parsed = XmlSimple.xml_in(response)
|
68
|
+
|
69
|
+
@batch_ids << response_parsed['id'][0]
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_batches
|
73
|
+
raise 'Records must be an array of hashes.' unless @records.is_a? Array
|
74
|
+
keys = @records.reduce({}) {|h, pairs| pairs.each {|k, v| (h[k] ||= []) << v}; h}.keys
|
75
|
+
|
76
|
+
@records_dup = @records.clone
|
77
|
+
|
78
|
+
super_records = []
|
79
|
+
(@records_dup.size / @batch_size).to_i.times do
|
80
|
+
super_records << @records_dup.pop(@batch_size)
|
81
|
+
end
|
82
|
+
super_records << @records_dup unless @records_dup.empty?
|
83
|
+
|
84
|
+
super_records.each do |batch|
|
85
|
+
@batch_ids << add_batch(keys, batch)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_batch(keys, batch)
|
90
|
+
xml = "#{@XML_HEADER}<sObjects xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
|
91
|
+
batch.each do |r|
|
92
|
+
xml += create_sobject(keys, r)
|
93
|
+
end
|
94
|
+
xml += '</sObjects>'
|
95
|
+
path = "job/#{@job_id}/batch/"
|
96
|
+
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
97
|
+
response = @connection.post_xml(nil, path, xml, headers)
|
98
|
+
response_parsed = XmlSimple.xml_in(response)
|
99
|
+
response_parsed['id'][0] if response_parsed['id']
|
100
|
+
end
|
101
|
+
|
102
|
+
def build_sobject(data)
|
103
|
+
xml = '<sObject>'
|
104
|
+
data.keys.each do |k|
|
105
|
+
if k.is_a?(Hash)
|
106
|
+
xml += build_sobject(k)
|
107
|
+
elsif data[k] != :type
|
108
|
+
xml += "<#{k}>#{data[k]}</#{k}>"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
xml += '</sObject>'
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_sobject(keys, r)
|
115
|
+
sobject_xml = '<sObject>'
|
116
|
+
keys.each do |k|
|
117
|
+
if r[k].is_a?(Hash)
|
118
|
+
sobject_xml += "<#{k}>"
|
119
|
+
sobject_xml += build_sobject(r[k])
|
120
|
+
sobject_xml += "</#{k}>"
|
121
|
+
elsif !r[k].to_s.empty?
|
122
|
+
sobject_xml += "<#{k}>"
|
123
|
+
if r[k].respond_to?(:encode)
|
124
|
+
sobject_xml += r[k].encode(:xml => :text)
|
125
|
+
elsif r[k].respond_to?(:iso8601) # timestamps
|
126
|
+
sobject_xml += r[k].iso8601.to_s
|
127
|
+
else
|
128
|
+
sobject_xml += r[k].to_s
|
129
|
+
end
|
130
|
+
sobject_xml += "</#{k}>"
|
131
|
+
elsif @send_nulls && !@no_null_list.include?(k)
|
132
|
+
sobject_xml += "<#{k} xsi:nil=\"true\"/>"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
sobject_xml += '</sObject>'
|
136
|
+
sobject_xml
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_job_status
|
140
|
+
path = "job/#{@job_id}"
|
141
|
+
headers = Hash.new
|
142
|
+
response = @connection.get_request(nil, path, headers)
|
143
|
+
|
144
|
+
begin
|
145
|
+
response_parsed = XmlSimple.xml_in(response) if response
|
146
|
+
response_parsed
|
147
|
+
rescue StandardError => e
|
148
|
+
puts "Error parsing XML response for #{@job_id}"
|
149
|
+
puts e
|
150
|
+
puts e.backtrace
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def check_batch_status(batch_id)
|
155
|
+
path = "job/#{@job_id}/batch/#{batch_id}"
|
156
|
+
headers = Hash.new
|
157
|
+
|
158
|
+
response = @connection.get_request(nil, path, headers)
|
159
|
+
|
160
|
+
begin
|
161
|
+
response_parsed = XmlSimple.xml_in(response) if response
|
162
|
+
response_parsed
|
163
|
+
rescue StandardError => e
|
164
|
+
puts "Error parsing XML response for #{@job_id}, batch #{batch_id}"
|
165
|
+
puts e
|
166
|
+
puts e.backtrace
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_job_result(return_result, timeout)
|
171
|
+
# timeout is in seconds
|
172
|
+
begin
|
173
|
+
state = []
|
174
|
+
Timeout::timeout(timeout, SalesforceBulkApiSerialOrParallel::JobTimeout) do
|
175
|
+
while true
|
176
|
+
job_status = self.check_job_status
|
177
|
+
if job_status && job_status['state'] && job_status['state'][0] == 'Closed'
|
178
|
+
batch_statuses = {}
|
179
|
+
|
180
|
+
batches_ready = @batch_ids.all? do |batch_id|
|
181
|
+
batch_state = batch_statuses[batch_id] = self.check_batch_status(batch_id)
|
182
|
+
batch_state && batch_state['state'] && batch_state['state'][0] && !['Queued', 'InProgress'].include?(batch_state['state'][0])
|
183
|
+
end
|
184
|
+
|
185
|
+
if batches_ready
|
186
|
+
@batch_ids.each do |batch_id|
|
187
|
+
state.insert(0, batch_statuses[batch_id])
|
188
|
+
@batch_ids.delete(batch_id)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
break if @batch_ids.empty?
|
192
|
+
else
|
193
|
+
break
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
rescue SalesforceBulkApiSerialOrParallel::JobTimeout => e
|
198
|
+
puts 'Timeout waiting for Salesforce to process job batches #{@batch_ids} of job #{@job_id}.'
|
199
|
+
puts e
|
200
|
+
raise
|
201
|
+
end
|
202
|
+
|
203
|
+
state.each_with_index do |batch_state, i|
|
204
|
+
if batch_state['state'][0] == 'Completed' && return_result == true
|
205
|
+
state[i].merge!({'response' => self.get_batch_result(batch_state['id'][0])})
|
206
|
+
end
|
207
|
+
end
|
208
|
+
state
|
209
|
+
end
|
210
|
+
|
211
|
+
def get_batch_result(batch_id)
|
212
|
+
path = "job/#{@job_id}/batch/#{batch_id}/result"
|
213
|
+
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
214
|
+
|
215
|
+
response = @connection.get_request(nil, path, headers)
|
216
|
+
response_parsed = XmlSimple.xml_in(response)
|
217
|
+
results = response_parsed['result'] unless @operation == 'query'
|
218
|
+
|
219
|
+
if(@operation == 'query') # The query op requires us to do another request to get the results
|
220
|
+
result_id = response_parsed["result"][0]
|
221
|
+
path = "job/#{@job_id}/batch/#{batch_id}/result/#{result_id}"
|
222
|
+
headers = Hash.new
|
223
|
+
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
224
|
+
response = @connection.get_request(nil, path, headers)
|
225
|
+
response_parsed = XmlSimple.xml_in(response)
|
226
|
+
results = response_parsed['records']
|
227
|
+
end
|
228
|
+
results
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
class JobTimeout < StandardError
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "salesforce_bulk_api_serial_or_parallel/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'salesforce_bulk_api_serial_or_parallel'
|
7
|
+
s.version = SalesforceBulkApiSerialOrParallel::VERSION
|
8
|
+
s.authors = ['Brendan Keogh']
|
9
|
+
s.email = ['bkeogh123@gmail.com']
|
10
|
+
|
11
|
+
s.homepage = 'https://github.com/beekermememe/salesforce_bulk_api_serial_or_parallel'
|
12
|
+
s.summary = %q{It uses the bulk api of salesforce to communicate with Salesforce CRM}
|
13
|
+
s.description = %q{Salesforce Bulk API with governor limits taken care of, this is a fork off of yatish27/salesforce_bulk_api_serial_or_parallel that just adds serial/parallel concurrency support}
|
14
|
+
|
15
|
+
s.rubyforge_project = 'salesforce_bulk_api_serial_or_parallel'
|
16
|
+
|
17
|
+
s.add_dependency('json', ['>= 0'])
|
18
|
+
s.add_dependency('xml-simple', ['>= 0'])
|
19
|
+
|
20
|
+
s.add_development_dependency 'rspec'
|
21
|
+
s.add_development_dependency 'restforce', '~> 1.5.1'
|
22
|
+
s.add_development_dependency 'rake', '~> 10.4.2'
|
23
|
+
s.add_development_dependency 'pry'
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
28
|
+
s.require_paths = ['lib']
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yaml'
|
3
|
+
require 'restforce'
|
4
|
+
|
5
|
+
describe SalesforceBulkApiSerialOrParallel do
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
auth_hash = YAML.load_file('auth_credentials.yml')
|
9
|
+
sfdc_auth_hash = auth_hash['salesforce']
|
10
|
+
|
11
|
+
@sf_client = Restforce.new(
|
12
|
+
username: sfdc_auth_hash['user'],
|
13
|
+
password: sfdc_auth_hash['passwordandtoken'],
|
14
|
+
client_id: sfdc_auth_hash['client_id'],
|
15
|
+
client_secret: sfdc_auth_hash['client_secret'],
|
16
|
+
host: sfdc_auth_hash['host'])
|
17
|
+
@sf_client.authenticate!
|
18
|
+
|
19
|
+
@account_id = auth_hash['salesforce']['test_account_id']
|
20
|
+
|
21
|
+
@api = SalesforceBulkApiSerialOrParallel::Api.new(@sf_client)
|
22
|
+
end
|
23
|
+
|
24
|
+
after :each do
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'upsert' do
|
29
|
+
|
30
|
+
context 'when not passed get_result' do
|
31
|
+
it "doesn't return the batches array" do
|
32
|
+
res = @api.upsert('Account', [{:Id => @account_id, :Website => 'www.test.com'}], 'Id')
|
33
|
+
res['batches'].should be_nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when passed get_result = true' do
|
38
|
+
it 'returns the batches array' do
|
39
|
+
res = @api.upsert('Account', [{:Id => @account_id, :Website => 'www.test.com'}], 'Id', true)
|
40
|
+
res['batches'][0]['response'].is_a? Array
|
41
|
+
|
42
|
+
res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
|
43
|
+
res['batches'][0]['response'][0]['success'].should eq ['true']
|
44
|
+
res['batches'][0]['response'][0]['created'].should eq ['false']
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when passed send_nulls = true' do
|
50
|
+
it 'sets the nil and empty attributes to NULL' do
|
51
|
+
@api.update('Account', [{:Id => @account_id, :Website => 'abc123', :Phone => '5678'}], true)
|
52
|
+
res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
|
53
|
+
res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
|
54
|
+
res['batches'][0]['response'][0]['Phone'][0].should eq '5678'
|
55
|
+
res = @api.upsert('Account', [{:Id => @account_id, :Website => '', :Phone => nil}], 'Id', true, true)
|
56
|
+
res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
|
57
|
+
res['batches'][0]['response'][0]['success'].should eq ['true']
|
58
|
+
res['batches'][0]['response'][0]['created'].should eq ['false']
|
59
|
+
res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
|
60
|
+
res['batches'][0]['response'][0]['Website'][0].should eq({"xsi:nil" => "true"})
|
61
|
+
res['batches'][0]['response'][0]['Phone'][0].should eq({"xsi:nil" => "true"})
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'when passed send_nulls = true and an array of fields not to null' do
|
66
|
+
it 'sets the nil and empty attributes to NULL, except for those included in the list of fields to ignore' do
|
67
|
+
@api.update('Account', [{:Id => @account_id, :Website => 'abc123', :Phone => '5678'}], true)
|
68
|
+
res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
|
69
|
+
res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
|
70
|
+
res['batches'][0]['response'][0]['Phone'][0].should eq '5678'
|
71
|
+
res = @api.upsert('Account', [{:Id => @account_id, :Website => '', :Phone => nil}], 'Id', true, true, [:Website, :Phone])
|
72
|
+
res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
|
73
|
+
res['batches'][0]['response'][0]['success'].should eq ['true']
|
74
|
+
res['batches'][0]['response'][0]['created'].should eq ['false']
|
75
|
+
res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
|
76
|
+
res['batches'][0]['response'][0]['Website'][0].should eq('abc123')
|
77
|
+
res['batches'][0]['response'][0]['Phone'][0].should eq('5678')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'update' do
|
84
|
+
context 'when there is not an error' do
|
85
|
+
context 'when not passed get_result' do
|
86
|
+
it "doesnt return the batches array" do
|
87
|
+
res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}])
|
88
|
+
res['batches'].should be_nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when passed get_result = true' do
|
93
|
+
it 'returns the batches array' do
|
94
|
+
res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}], true)
|
95
|
+
res['batches'][0]['response'].is_a? Array
|
96
|
+
res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
|
97
|
+
res['batches'][0]['response'][0]['success'].should eq ['true']
|
98
|
+
res['batches'][0]['response'][0]['created'].should eq ['false']
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'when there is an error' do
|
104
|
+
context 'when not passed get_result' do
|
105
|
+
it "doesn't return the results array" do
|
106
|
+
res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'},{:Id => 'abc123', :Website => 'www.test.com'}])
|
107
|
+
res['batches'].should be_nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'when passed get_result = true with batches' do
|
112
|
+
it 'returns the results array' do
|
113
|
+
res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}, {:Id => @account_id, :Website => 'www.test.com'}, {:Id => @account_id, :Website => 'www.test.com'}, {:Id => 'abc123', :Website => 'www.test.com'}], true, false, [], 2)
|
114
|
+
|
115
|
+
res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
|
116
|
+
res['batches'][0]['response'][0]['success'].should eq ['true']
|
117
|
+
res['batches'][0]['response'][0]['created'].should eq ['false']
|
118
|
+
res['batches'][0]['response'][1]['id'][0].should start_with(@account_id)
|
119
|
+
res['batches'][0]['response'][1]['success'].should eq ['true']
|
120
|
+
res['batches'][0]['response'][1]['created'].should eq ['false']
|
121
|
+
|
122
|
+
res['batches'][1]['response'][0]['id'][0].should start_with(@account_id)
|
123
|
+
res['batches'][1]['response'][0]['success'].should eq ['true']
|
124
|
+
res['batches'][1]['response'][0]['created'].should eq ['false']
|
125
|
+
res['batches'][1]['response'][1].should eq({"errors"=>[{"fields"=>["Id"], "message"=>["Account ID: id value of incorrect type: abc123"], "statusCode"=>["MALFORMED_ID"]}], "success"=>["false"], "created"=>["false"]})
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
describe 'create' do
|
133
|
+
pending
|
134
|
+
end
|
135
|
+
|
136
|
+
describe 'delete' do
|
137
|
+
pending
|
138
|
+
end
|
139
|
+
|
140
|
+
describe 'query' do
|
141
|
+
|
142
|
+
context 'when there are results' do
|
143
|
+
it 'returns the query results' do
|
144
|
+
res = @api.query('Account', "SELECT id, Name From Account WHERE Name LIKE 'Test%'")
|
145
|
+
res['batches'][0]['response'].length.should > 1
|
146
|
+
res['batches'][0]['response'][0]['Id'].should_not be_nil
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'and there are multiple batches' do
|
150
|
+
# need dev to create > 10k records in dev organization
|
151
|
+
it 'returns the query results in a merged hash'
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'when there are no results' do
|
156
|
+
it 'returns nil' do
|
157
|
+
res = @api.query('Account', "SELECT id From Account WHERE Name = 'ABC'")
|
158
|
+
res['batches'][0]['response'].should eq nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'when there is an error' do
|
163
|
+
it 'returns nil' do
|
164
|
+
res = @api.query('Account', "SELECT id From Account WHERE Name = ''ABC'")
|
165
|
+
res['batches'][0]['response'].should eq nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
describe 'counters' do
|
172
|
+
context 'when read operations are called' do
|
173
|
+
it 'increments operation count and http GET count' do
|
174
|
+
@api.counters[:http_get].should eq 0
|
175
|
+
@api.counters[:query].should eq 0
|
176
|
+
@api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
|
177
|
+
@api.counters[:http_get].should eq 1
|
178
|
+
@api.counters[:query].should eq 1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
context 'when update operations are called' do
|
183
|
+
it 'increments operation count and http POST count' do
|
184
|
+
@api.counters[:http_post].should eq 0
|
185
|
+
@api.counters[:update].should eq 0
|
186
|
+
@api.update('Account', [{:Id => @account_id, :Website => 'abc123', :Phone => '5678'}], true)
|
187
|
+
@api.counters[:http_post].should eq 1
|
188
|
+
@api.counters[:update].should eq 1
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: salesforce_bulk_api_serial_or_parallel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brendan Keogh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: xml-simple
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: restforce
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.5.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.5.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 10.4.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 10.4.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Salesforce Bulk API with governor limits taken care of, this is a fork
|
98
|
+
off of yatish27/salesforce_bulk_api_serial_or_parallel that just adds serial/parallel
|
99
|
+
concurrency support
|
100
|
+
email:
|
101
|
+
- bkeogh123@gmail.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- ".gitignore"
|
107
|
+
- ".rspec"
|
108
|
+
- Gemfile
|
109
|
+
- LICENCE
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- auth_credentials.yml.example
|
113
|
+
- lib/salesforce_bulk_api_serial_or_parallel.rb
|
114
|
+
- lib/salesforce_bulk_api_serial_or_parallel/concerns/throttling.rb
|
115
|
+
- lib/salesforce_bulk_api_serial_or_parallel/connection.rb
|
116
|
+
- lib/salesforce_bulk_api_serial_or_parallel/job.rb
|
117
|
+
- lib/salesforce_bulk_api_serial_or_parallel/version.rb
|
118
|
+
- salesforce_bulk_api_serial_or_parallel.gemspec
|
119
|
+
- spec/salesforce_bulk_api_serial_or_parallel/salesforce_bulk_api_serial_or_parallel.rb
|
120
|
+
- spec/spec_helper.rb
|
121
|
+
homepage: https://github.com/beekermememe/salesforce_bulk_api_serial_or_parallel
|
122
|
+
licenses: []
|
123
|
+
metadata: {}
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project: salesforce_bulk_api_serial_or_parallel
|
140
|
+
rubygems_version: 2.6.13
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: It uses the bulk api of salesforce to communicate with Salesforce CRM
|
144
|
+
test_files:
|
145
|
+
- spec/salesforce_bulk_api_serial_or_parallel/salesforce_bulk_api_serial_or_parallel.rb
|
146
|
+
- spec/spec_helper.rb
|