salesforce_bulk_api 0.0.8 → 1.0.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 +5 -5
- data/.gitignore +2 -0
- data/LICENCE +21 -0
- data/README.md +112 -62
- data/auth_credentials.yml.example +7 -0
- data/lib/salesforce_bulk_api.rb +59 -14
- data/lib/salesforce_bulk_api/concerns/throttling.rb +60 -0
- data/lib/salesforce_bulk_api/connection.rb +44 -30
- data/lib/salesforce_bulk_api/job.rb +83 -16
- data/lib/salesforce_bulk_api/version.rb +1 -1
- data/salesforce_bulk_api.gemspec +12 -13
- data/spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb +87 -39
- data/spec/spec_helper.rb +0 -8
- metadata +40 -25
- data/example_auth_credentials.yml +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bd5b061f299027dc456e2c65f6b33480c7192d9c5af15a5fecd3cd1b81b79589
|
4
|
+
data.tar.gz: 4e5a70fe4541df2b944bdb3f59c0acdc78f91d8f2100ae164c03a23e54e09031
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b453618a4cddde3851d766ca672df92177bae019152192bf628ebd2f8e08e92f2662226daf85bf81ea92d64a7e9756ccd7a495e170f32924985f4bbbd5c4b727
|
7
|
+
data.tar.gz: f44136ac331cc22b288f8f27a87b8f5def897f4aca051dcc3c2e1dd860fb1d2f9fb0d337b40858f63cbdeabdc407644ea69ed3a1f129491ba6f50a12f4e893ac
|
data/.gitignore
CHANGED
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
CHANGED
@@ -1,91 +1,141 @@
|
|
1
1
|
# Salesforce-Bulk-Api
|
2
2
|
[](http://badge.fury.io/rb/salesforce_bulk_api)
|
3
|
+
|
3
4
|
## Overview
|
4
5
|
|
5
|
-
|
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`.
|
6
9
|
|
7
10
|
## How to use
|
8
11
|
|
9
12
|
Using this gem is simple and straight forward.
|
10
13
|
|
11
|
-
|
14
|
+
### Install
|
12
15
|
|
13
|
-
`
|
16
|
+
`gem install salesforce_bulk_api`
|
14
17
|
|
15
|
-
or add
|
18
|
+
or add it to your Gemfile
|
16
19
|
|
17
20
|
`gem salesforce_bulk_api`
|
18
|
-
|
19
|
-
in your Gemfile
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
### Authenticate
|
23
|
+
|
24
|
+
You can authenticate with Salesforce using two gems, `databasedotcom` & `restforce`.
|
25
|
+
|
26
|
+
Please check the documentation of the respective gems to learn how to authenticate with Salesforce
|
23
27
|
|
24
28
|
[Databasedotcom](https://github.com/heroku/databasedotcom)
|
25
29
|
[Restforce](https://github.com/ejholmes/restforce)
|
26
30
|
|
27
|
-
|
28
31
|
You can use username password combo, OmniAuth, Oauth2
|
29
32
|
You can use as many records possible in the Array. Governor limits are taken care of inside the gem.
|
30
33
|
|
34
|
+
```ruby
|
35
|
+
require 'salesforce_bulk_api'
|
31
36
|
|
32
|
-
|
33
|
-
|
34
|
-
|
37
|
+
client = Databasedotcom::Client.new(
|
38
|
+
:client_id => SFDC_APP_CONFIG["client_id"],
|
39
|
+
:client_secret => SFDC_APP_CONFIG["client_secret"]
|
40
|
+
)
|
41
|
+
client.authenticate(
|
42
|
+
:token => " ",
|
43
|
+
:instance_url => "http://na1.salesforce.com"
|
44
|
+
)
|
35
45
|
|
36
|
-
|
46
|
+
salesforce = SalesforceBulkApi::Api.new(client)
|
47
|
+
```
|
37
48
|
|
38
49
|
OR
|
39
50
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
51
|
+
```ruby
|
52
|
+
require 'salesforce_bulk_api'
|
53
|
+
client = Restforce.new(
|
54
|
+
username: SFDC_APP_CONFIG['SFDC_USERNAME'],
|
55
|
+
password: SFDC_APP_CONFIG['SFDC_PASSWORD'],
|
56
|
+
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']
|
60
|
+
)
|
61
|
+
client.authenticate!
|
62
|
+
|
63
|
+
salesforce = SalesforceBulkApi::Api.new(client)
|
64
|
+
```
|
65
|
+
|
66
|
+
### Sample operations:
|
67
|
+
|
68
|
+
```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)
|
75
|
+
result = salesforce.create("Account", records_to_insert)
|
76
|
+
puts "result is: #{result.inspect}"
|
77
|
+
|
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)
|
82
|
+
salesforce.update("Account", records_to_update)
|
83
|
+
|
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
|
89
|
+
|
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)
|
94
|
+
salesforce.delete("Account", records_to_delete)
|
95
|
+
|
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
|
98
|
+
```
|
99
|
+
|
100
|
+
### Helpful methods:
|
101
|
+
|
102
|
+
```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}"
|
106
|
+
```
|
107
|
+
|
108
|
+
### Listening to events:
|
109
|
+
|
110
|
+
```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
|
+
salesforce.on_job_created do |job|
|
115
|
+
puts "Job #{job.job_id} created!"
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
### Fetching records from a batch
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
job_id = 'l02A0231009Za8m'
|
123
|
+
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
|
+
|
129
|
+
```
|
130
|
+
|
131
|
+
### Throttling API calls:
|
132
|
+
|
133
|
+
```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
|
137
|
+
```
|
138
|
+
|
89
139
|
## Contribute
|
90
140
|
|
91
141
|
Feel to fork and send Pull request
|
data/lib/salesforce_bulk_api.rb
CHANGED
@@ -1,53 +1,98 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler'
|
3
|
-
Bundler.require()
|
4
|
-
require "salesforce_bulk_api/version"
|
5
3
|
require 'net/https'
|
6
4
|
require 'xmlsimple'
|
7
5
|
require 'csv'
|
6
|
+
|
7
|
+
require 'salesforce_bulk_api/version'
|
8
|
+
require 'salesforce_bulk_api/concerns/throttling'
|
8
9
|
require 'salesforce_bulk_api/job'
|
9
10
|
require 'salesforce_bulk_api/connection'
|
10
11
|
|
11
12
|
module SalesforceBulkApi
|
12
|
-
|
13
13
|
class Api
|
14
|
+
attr_reader :connection
|
14
15
|
|
15
|
-
|
16
|
+
SALESFORCE_API_VERSION = '46.0'
|
16
17
|
|
17
18
|
def initialize(client)
|
18
|
-
@connection = SalesforceBulkApi::Connection.new(
|
19
|
+
@connection = SalesforceBulkApi::Connection.new(SALESFORCE_API_VERSION, client)
|
20
|
+
@listeners = { job_created: [] }
|
19
21
|
end
|
20
22
|
|
21
23
|
def upsert(sobject, records, external_field, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500)
|
22
|
-
|
24
|
+
do_operation('upsert', sobject, records, external_field, get_response, timeout, batch_size, send_nulls, no_null_list)
|
23
25
|
end
|
24
26
|
|
25
27
|
def update(sobject, records, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500)
|
26
|
-
|
28
|
+
do_operation('update', sobject, records, nil, get_response, timeout, batch_size, send_nulls, no_null_list)
|
27
29
|
end
|
28
30
|
|
29
31
|
def create(sobject, records, get_response = false, send_nulls = false, batch_size = 10000, timeout = 1500)
|
30
|
-
|
32
|
+
do_operation('insert', sobject, records, nil, get_response, timeout, batch_size, send_nulls)
|
31
33
|
end
|
32
34
|
|
33
35
|
def delete(sobject, records, get_response = false, batch_size = 10000, timeout = 1500)
|
34
|
-
|
36
|
+
do_operation('delete', sobject, records, nil, get_response, timeout, batch_size)
|
35
37
|
end
|
36
38
|
|
37
39
|
def query(sobject, query, batch_size = 10000, timeout = 1500)
|
38
|
-
|
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
|
+
}
|
39
53
|
end
|
40
54
|
|
41
|
-
#
|
55
|
+
# Allows you to attach a listener that accepts the created job (which has a useful #job_id field). This is useful
|
56
|
+
# for recording a job ID persistently before you begin batch work (i.e. start modifying the salesforce database),
|
57
|
+
# so if the load process you are writing needs to recover, it can be aware of previous jobs it started and wait
|
58
|
+
# for them to finish.
|
59
|
+
#
|
60
|
+
def on_job_created(&block)
|
61
|
+
@listeners[:job_created] << block
|
62
|
+
end
|
63
|
+
|
64
|
+
def job_from_id(job_id)
|
65
|
+
SalesforceBulkApi::Job.new(job_id: job_id, connection: @connection)
|
66
|
+
end
|
42
67
|
|
43
68
|
def do_operation(operation, sobject, records, external_field, get_response, timeout, batch_size, send_nulls = false, no_null_list = [])
|
44
|
-
|
69
|
+
count operation.to_sym
|
70
|
+
|
71
|
+
job = SalesforceBulkApi::Job.new(
|
72
|
+
operation: operation,
|
73
|
+
sobject: sobject,
|
74
|
+
records: records,
|
75
|
+
external_field: external_field,
|
76
|
+
connection: @connection
|
77
|
+
)
|
45
78
|
|
46
79
|
job.create_job(batch_size, send_nulls, no_null_list)
|
80
|
+
@listeners[:job_created].each {|callback| callback.call(job)}
|
47
81
|
operation == "query" ? job.add_query() : job.add_batches()
|
48
82
|
response = job.close_job
|
49
83
|
response.merge!({'batches' => job.get_job_result(get_response, timeout)}) if get_response == true
|
50
84
|
response
|
51
85
|
end
|
52
|
-
|
53
|
-
|
86
|
+
|
87
|
+
private
|
88
|
+
|
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 SalesforceBulkApi::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
|
@@ -1,49 +1,43 @@
|
|
1
|
-
module SalesforceBulkApi
|
2
1
|
require 'timeout'
|
3
2
|
|
3
|
+
module SalesforceBulkApi
|
4
4
|
class Connection
|
5
|
+
include Concerns::Throttling
|
5
6
|
|
6
|
-
|
7
|
-
@@API_VERSION = nil
|
8
|
-
@@LOGIN_HOST = 'login.salesforce.com'
|
9
|
-
@@INSTANCE_HOST = nil # Gets set in login()
|
7
|
+
LOGIN_HOST = 'login.salesforce.com'
|
10
8
|
|
11
|
-
def initialize(api_version,client)
|
12
|
-
@client=client
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@instance = nil
|
16
|
-
@@API_VERSION = api_version
|
17
|
-
@@LOGIN_PATH = "/services/Soap/u/#{@@API_VERSION}"
|
18
|
-
@@PATH_PREFIX = "/services/async/#{@@API_VERSION}/"
|
9
|
+
def initialize(api_version, client)
|
10
|
+
@client = client
|
11
|
+
@api_version = api_version
|
12
|
+
@path_prefix = "/services/async/#{@api_version}/"
|
19
13
|
|
20
14
|
login()
|
21
15
|
end
|
22
16
|
|
23
|
-
#private
|
24
|
-
|
25
17
|
def login()
|
26
18
|
client_type = @client.class.to_s
|
27
19
|
case client_type
|
28
20
|
when "Restforce::Data::Client"
|
29
|
-
@session_id
|
30
|
-
@server_url
|
21
|
+
@session_id = @client.options[:oauth_token]
|
22
|
+
@server_url = @client.options[:instance_url]
|
31
23
|
else
|
32
|
-
@session_id
|
33
|
-
@server_url
|
24
|
+
@session_id = @client.oauth_token
|
25
|
+
@server_url = @client.instance_url
|
34
26
|
end
|
35
27
|
@instance = parse_instance()
|
36
|
-
|
28
|
+
@instance_host = "#{@instance}.salesforce.com"
|
37
29
|
end
|
38
30
|
|
39
31
|
def post_xml(host, path, xml, headers)
|
40
|
-
host = host ||
|
41
|
-
if host !=
|
42
|
-
headers['X-SFDC-Session'] = @session_id
|
43
|
-
path = "#{
|
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}"
|
44
36
|
end
|
45
37
|
i = 0
|
46
38
|
begin
|
39
|
+
count :post
|
40
|
+
throttle(http_method: :post, path: path)
|
47
41
|
https(host).post(path, xml, headers).body
|
48
42
|
rescue
|
49
43
|
i += 1
|
@@ -58,11 +52,14 @@ require 'timeout'
|
|
58
52
|
end
|
59
53
|
|
60
54
|
def get_request(host, path, headers)
|
61
|
-
host = host ||
|
62
|
-
path = "#{
|
63
|
-
if host !=
|
55
|
+
host = host || @instance_host
|
56
|
+
path = "#{@path_prefix}#{path}"
|
57
|
+
if host != LOGIN_HOST # Not login, need to add session id to header
|
64
58
|
headers['X-SFDC-Session'] = @session_id;
|
65
59
|
end
|
60
|
+
|
61
|
+
count :get
|
62
|
+
throttle(http_method: :get, path: path)
|
66
63
|
https(host).get(path, headers).body
|
67
64
|
end
|
68
65
|
|
@@ -73,10 +70,27 @@ require 'timeout'
|
|
73
70
|
req
|
74
71
|
end
|
75
72
|
|
73
|
+
def counters
|
74
|
+
{
|
75
|
+
get: get_counters[:get],
|
76
|
+
post: get_counters[:post]
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def get_counters
|
83
|
+
@counters ||= Hash.new(0)
|
84
|
+
end
|
85
|
+
|
86
|
+
def count(http_method)
|
87
|
+
get_counters[http_method] += 1
|
88
|
+
end
|
89
|
+
|
76
90
|
def parse_instance()
|
77
|
-
@instance
|
78
|
-
@instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.
|
79
|
-
|
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
|
80
94
|
end
|
81
95
|
|
82
96
|
end
|
@@ -1,13 +1,17 @@
|
|
1
1
|
module SalesforceBulkApi
|
2
2
|
|
3
3
|
class Job
|
4
|
+
attr_reader :job_id
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
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]
|
11
15
|
@batch_ids = []
|
12
16
|
@XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>'
|
13
17
|
end
|
@@ -20,7 +24,8 @@ module SalesforceBulkApi
|
|
20
24
|
xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
|
21
25
|
xml += "<operation>#{@operation}</operation>"
|
22
26
|
xml += "<object>#{@sobject}</object>"
|
23
|
-
|
27
|
+
# This only happens on upsert
|
28
|
+
if !@external_field.nil?
|
24
29
|
xml += "<externalIdFieldName>#{@external_field}</externalIdFieldName>"
|
25
30
|
end
|
26
31
|
xml += "<contentType>XML</contentType>"
|
@@ -31,6 +36,10 @@ module SalesforceBulkApi
|
|
31
36
|
|
32
37
|
response = @connection.post_xml(nil, path, xml, headers)
|
33
38
|
response_parsed = XmlSimple.xml_in(response)
|
39
|
+
|
40
|
+
# response may contain an exception, so raise it
|
41
|
+
raise SalesforceException.new("#{response_parsed['exceptionMessage'][0]} (#{response_parsed['exceptionCode'][0]})") if response_parsed['exceptionCode']
|
42
|
+
|
34
43
|
@job_id = response_parsed['id'][0]
|
35
44
|
end
|
36
45
|
|
@@ -58,12 +67,12 @@ module SalesforceBulkApi
|
|
58
67
|
|
59
68
|
def add_batches
|
60
69
|
raise 'Records must be an array of hashes.' unless @records.is_a? Array
|
61
|
-
keys = @records.reduce({}) {|h,pairs| pairs.each {|k,v| (h[k] ||= []) << v}; h}.keys
|
70
|
+
keys = @records.reduce({}) {|h, pairs| pairs.each {|k, v| (h[k] ||= []) << v}; h}.keys
|
62
71
|
|
63
72
|
@records_dup = @records.clone
|
64
73
|
|
65
74
|
super_records = []
|
66
|
-
(@records_dup.size
|
75
|
+
(@records_dup.size / @batch_size).to_i.times do
|
67
76
|
super_records << @records_dup.pop(@batch_size)
|
68
77
|
end
|
69
78
|
super_records << @records_dup unless @records_dup.empty?
|
@@ -86,18 +95,58 @@ module SalesforceBulkApi
|
|
86
95
|
response_parsed['id'][0] if response_parsed['id']
|
87
96
|
end
|
88
97
|
|
98
|
+
def build_sobject(data)
|
99
|
+
xml = '<sObject>'
|
100
|
+
data.keys.each do |k|
|
101
|
+
if k.is_a?(Hash)
|
102
|
+
xml += build_sobject(k)
|
103
|
+
elsif k.to_s.include? '.'
|
104
|
+
relations = k.to_s.split('.')
|
105
|
+
parent = relations[0]
|
106
|
+
child = relations[1..-1].join('.')
|
107
|
+
xml += "<#{parent}>#{build_sobject({ child => data[k] })}</#{parent}>"
|
108
|
+
elsif data[k] != :type
|
109
|
+
xml += "<#{k}>#{data[k]}</#{k}>"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
xml += '</sObject>'
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_relationship_sobject(key, value)
|
116
|
+
if key.to_s.include? '.'
|
117
|
+
relations = key.to_s.split('.')
|
118
|
+
parent = relations[0]
|
119
|
+
child = relations[1..-1].join('.')
|
120
|
+
xml = "<#{parent}>"
|
121
|
+
xml += "<sObject>"
|
122
|
+
xml += build_relationship_sobject(child, value)
|
123
|
+
xml += "</sObject>"
|
124
|
+
xml += "</#{parent}>"
|
125
|
+
else
|
126
|
+
xml = "<#{key}>#{value}</#{key}>"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
89
130
|
def create_sobject(keys, r)
|
90
131
|
sobject_xml = '<sObject>'
|
91
132
|
keys.each do |k|
|
92
|
-
if
|
133
|
+
if r[k].is_a?(Hash)
|
134
|
+
sobject_xml += "<#{k}>"
|
135
|
+
sobject_xml += build_sobject(r[k])
|
136
|
+
sobject_xml += "</#{k}>"
|
137
|
+
elsif k.to_s.include? '.'
|
138
|
+
sobject_xml += build_relationship_sobject(k, r[k])
|
139
|
+
elsif !r[k].to_s.empty?
|
93
140
|
sobject_xml += "<#{k}>"
|
94
141
|
if r[k].respond_to?(:encode)
|
95
142
|
sobject_xml += r[k].encode(:xml => :text)
|
143
|
+
elsif r[k].respond_to?(:iso8601) # timestamps
|
144
|
+
sobject_xml += r[k].iso8601.to_s
|
96
145
|
else
|
97
146
|
sobject_xml += r[k].to_s
|
98
147
|
end
|
99
148
|
sobject_xml += "</#{k}>"
|
100
|
-
elsif @send_nulls && !@no_null_list.include?(k)
|
149
|
+
elsif @send_nulls && !@no_null_list.include?(k) && r.key?(k)
|
101
150
|
sobject_xml += "<#{k} xsi:nil=\"true\"/>"
|
102
151
|
end
|
103
152
|
end
|
@@ -142,11 +191,18 @@ module SalesforceBulkApi
|
|
142
191
|
state = []
|
143
192
|
Timeout::timeout(timeout, SalesforceBulkApi::JobTimeout) do
|
144
193
|
while true
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
194
|
+
job_status = self.check_job_status
|
195
|
+
if job_status && job_status['state'] && job_status['state'][0] == 'Closed'
|
196
|
+
batch_statuses = {}
|
197
|
+
|
198
|
+
batches_ready = @batch_ids.all? do |batch_id|
|
199
|
+
batch_state = batch_statuses[batch_id] = self.check_batch_status(batch_id)
|
200
|
+
batch_state && batch_state['state'] && batch_state['state'][0] && !['Queued', 'InProgress'].include?(batch_state['state'][0])
|
201
|
+
end
|
202
|
+
|
203
|
+
if batches_ready
|
204
|
+
@batch_ids.each do |batch_id|
|
205
|
+
state.insert(0, batch_statuses[batch_id])
|
150
206
|
@batch_ids.delete(batch_id)
|
151
207
|
end
|
152
208
|
end
|
@@ -190,6 +246,17 @@ module SalesforceBulkApi
|
|
190
246
|
results
|
191
247
|
end
|
192
248
|
|
249
|
+
def get_batch_records(batch_id)
|
250
|
+
path = "job/#{@job_id}/batch/#{batch_id}/request"
|
251
|
+
headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
|
252
|
+
|
253
|
+
response = @connection.get_request(nil, path, headers)
|
254
|
+
response_parsed = XmlSimple.xml_in(response)
|
255
|
+
results = response_parsed['sObject']
|
256
|
+
|
257
|
+
results
|
258
|
+
end
|
259
|
+
|
193
260
|
end
|
194
261
|
|
195
262
|
class JobTimeout < StandardError
|
data/salesforce_bulk_api.gemspec
CHANGED
@@ -3,27 +3,26 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
3
3
|
require "salesforce_bulk_api/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.name =
|
6
|
+
s.name = 'salesforce_bulk_api'
|
7
7
|
s.version = SalesforceBulkApi::VERSION
|
8
|
-
s.authors = [
|
9
|
-
s.email = [
|
8
|
+
s.authors = ['Yatish Mehta']
|
9
|
+
s.email = ['yatishmehta27@gmail.com']
|
10
10
|
|
11
|
-
s.homepage =
|
11
|
+
s.homepage = 'https://github.com/yatishmehta27/salesforce_bulk_api'
|
12
12
|
s.summary = %q{It uses the bulk api of salesforce to communicate with Salesforce CRM}
|
13
13
|
s.description = %q{Salesforce Bulk API with governor limits taken care of}
|
14
14
|
|
15
|
-
s.
|
15
|
+
s.add_dependency('json', ['>= 0'])
|
16
|
+
s.add_dependency('xml-simple', ['>= 0'])
|
17
|
+
|
18
|
+
s.add_development_dependency 'rspec'
|
19
|
+
s.add_development_dependency 'restforce', '~> 3.0.0'
|
20
|
+
s.add_development_dependency "rake", ">= 12.3.3"
|
21
|
+
s.add_development_dependency 'pry'
|
16
22
|
|
17
|
-
s.add_dependency(%q<json>, [">= 0"])
|
18
|
-
s.add_dependency(%q<xml-simple>, [">= 0"])
|
19
|
-
|
20
|
-
s.add_development_dependency "rspec"
|
21
|
-
s.add_development_dependency("webmock", ["~> 1.13"])
|
22
|
-
s.add_development_dependency("vcr", ['~> 2.5'])
|
23
|
-
|
24
23
|
s.files = `git ls-files`.split("\n")
|
25
24
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
26
25
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
27
|
-
s.require_paths = [
|
26
|
+
s.require_paths = ['lib']
|
28
27
|
|
29
28
|
end
|
@@ -1,63 +1,80 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'yaml'
|
3
|
-
require '
|
3
|
+
require 'restforce'
|
4
4
|
|
5
5
|
describe SalesforceBulkApi do
|
6
6
|
|
7
7
|
before :each do
|
8
|
-
auth_hash = YAML.
|
9
|
-
|
10
|
-
|
11
|
-
@sf_client.
|
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
|
+
|
12
21
|
@api = SalesforceBulkApi::Api.new(@sf_client)
|
13
22
|
end
|
14
23
|
|
24
|
+
after :each do
|
25
|
+
|
26
|
+
end
|
27
|
+
|
15
28
|
describe 'upsert' do
|
16
29
|
|
17
30
|
context 'when not passed get_result' do
|
18
31
|
it "doesn't return the batches array" do
|
19
|
-
res = @api.upsert('Account', [{:Id =>
|
32
|
+
res = @api.upsert('Account', [{:Id => @account_id, :Website => 'www.test.com'}], 'Id')
|
20
33
|
res['batches'].should be_nil
|
21
34
|
end
|
22
35
|
end
|
23
36
|
|
24
37
|
context 'when passed get_result = true' do
|
25
38
|
it 'returns the batches array' do
|
26
|
-
res = @api.upsert('Account', [{:Id =>
|
39
|
+
res = @api.upsert('Account', [{:Id => @account_id, :Website => 'www.test.com'}], 'Id', true)
|
27
40
|
res['batches'][0]['response'].is_a? Array
|
28
|
-
|
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
|
+
|
29
46
|
end
|
30
47
|
end
|
31
48
|
|
32
49
|
context 'when passed send_nulls = true' do
|
33
50
|
it 'sets the nil and empty attributes to NULL' do
|
34
|
-
@api.update('Account', [{:Id =>
|
35
|
-
res = @api.query('Account', "SELECT Website,
|
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}'")
|
36
53
|
res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
|
37
|
-
res['batches'][0]['response'][0]['
|
38
|
-
res = @api.upsert('Account', [{:Id =>
|
39
|
-
res['batches'][0]['response'][0]
|
40
|
-
res
|
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}'")
|
41
60
|
res['batches'][0]['response'][0]['Website'][0].should eq({"xsi:nil" => "true"})
|
42
|
-
res['batches'][0]['response'][0]['
|
43
|
-
res['batches'][0]['response'][0]['Gold_Star__c'][0].should eq('false')
|
44
|
-
res['batches'][0]['response'][0]['CRM_Last_Modified__c'][0].should eq({"xsi:nil" => "true"})
|
61
|
+
res['batches'][0]['response'][0]['Phone'][0].should eq({"xsi:nil" => "true"})
|
45
62
|
end
|
46
63
|
end
|
47
64
|
|
48
65
|
context 'when passed send_nulls = true and an array of fields not to null' do
|
49
66
|
it 'sets the nil and empty attributes to NULL, except for those included in the list of fields to ignore' do
|
50
|
-
@api.update('Account', [{:Id =>
|
51
|
-
res = @api.query('Account', "SELECT Website,
|
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}'")
|
52
69
|
res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
|
53
|
-
res['batches'][0]['response'][0]['
|
54
|
-
res = @api.upsert('Account', [{:Id =>
|
55
|
-
res['batches'][0]['response'][0]
|
56
|
-
res
|
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}'")
|
57
76
|
res['batches'][0]['response'][0]['Website'][0].should eq('abc123')
|
58
|
-
res['batches'][0]['response'][0]['
|
59
|
-
res['batches'][0]['response'][0]['Gold_Star__c'][0].should eq('false')
|
60
|
-
res['batches'][0]['response'][0]['CRM_Last_Modified__c'][0].should eq({"xsi:nil" => "true"})
|
77
|
+
res['batches'][0]['response'][0]['Phone'][0].should eq('5678')
|
61
78
|
end
|
62
79
|
end
|
63
80
|
|
@@ -67,16 +84,18 @@ describe SalesforceBulkApi do
|
|
67
84
|
context 'when there is not an error' do
|
68
85
|
context 'when not passed get_result' do
|
69
86
|
it "doesnt return the batches array" do
|
70
|
-
res = @api.update('Account', [{:Id =>
|
87
|
+
res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}])
|
71
88
|
res['batches'].should be_nil
|
72
89
|
end
|
73
90
|
end
|
74
91
|
|
75
92
|
context 'when passed get_result = true' do
|
76
93
|
it 'returns the batches array' do
|
77
|
-
res = @api.update('Account', [{:Id =>
|
94
|
+
res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}], true)
|
78
95
|
res['batches'][0]['response'].is_a? Array
|
79
|
-
res['batches'][0]['response'][0]
|
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']
|
80
99
|
end
|
81
100
|
end
|
82
101
|
end
|
@@ -84,16 +103,26 @@ describe SalesforceBulkApi do
|
|
84
103
|
context 'when there is an error' do
|
85
104
|
context 'when not passed get_result' do
|
86
105
|
it "doesn't return the results array" do
|
87
|
-
res = @api.update('Account', [{:Id =>
|
106
|
+
res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'},{:Id => 'abc123', :Website => 'www.test.com'}])
|
88
107
|
res['batches'].should be_nil
|
89
108
|
end
|
90
109
|
end
|
91
110
|
|
92
111
|
context 'when passed get_result = true with batches' do
|
93
112
|
it 'returns the results array' do
|
94
|
-
res = @api.update('Account', [{:Id =>
|
95
|
-
|
96
|
-
res['batches'][
|
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"]})
|
97
126
|
end
|
98
127
|
end
|
99
128
|
end
|
@@ -116,13 +145,10 @@ describe SalesforceBulkApi do
|
|
116
145
|
res['batches'][0]['response'].length.should > 1
|
117
146
|
res['batches'][0]['response'][0]['Id'].should_not be_nil
|
118
147
|
end
|
148
|
+
|
119
149
|
context 'and there are multiple batches' do
|
120
|
-
|
121
|
-
|
122
|
-
res = @api.query('Account', "SELECT id, Name From Account WHERE Name LIKE 'Test%'")
|
123
|
-
res['batches'][0]['response'].length.should > 1
|
124
|
-
res['batches'][0]['response'][0]['Id'].should_not be_nil
|
125
|
-
end
|
150
|
+
# need dev to create > 10k records in dev organization
|
151
|
+
it 'returns the query results in a merged hash'
|
126
152
|
end
|
127
153
|
end
|
128
154
|
|
@@ -142,4 +168,26 @@ describe SalesforceBulkApi do
|
|
142
168
|
|
143
169
|
end
|
144
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
|
+
|
145
193
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,16 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
|
-
#require 'webmock/rspec'
|
4
|
-
#require 'vcr'
|
5
3
|
require 'salesforce_bulk_api'
|
6
4
|
|
7
5
|
RSpec.configure do |c|
|
8
6
|
c.filter_run :focus => true
|
9
7
|
c.run_all_when_everything_filtered = true
|
10
8
|
end
|
11
|
-
|
12
|
-
# enable this and record the test requests using a SF developer org.
|
13
|
-
# VCR.configure do |c|
|
14
|
-
# c.cassette_library_dir = 'spec/cassettes'
|
15
|
-
# c.hook_into :webmock
|
16
|
-
# end
|
metadata
CHANGED
@@ -1,85 +1,99 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: salesforce_bulk_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yatish Mehta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: xml-simple
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: restforce
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - ~>
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 3.0.0
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - ~>
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 3.0.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: 12.3.3
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: 12.3.3
|
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'
|
83
97
|
description: Salesforce Bulk API with governor limits taken care of
|
84
98
|
email:
|
85
99
|
- yatishmehta27@gmail.com
|
@@ -87,13 +101,15 @@ executables: []
|
|
87
101
|
extensions: []
|
88
102
|
extra_rdoc_files: []
|
89
103
|
files:
|
90
|
-
- .gitignore
|
91
|
-
- .rspec
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
92
106
|
- Gemfile
|
107
|
+
- LICENCE
|
93
108
|
- README.md
|
94
109
|
- Rakefile
|
95
|
-
-
|
110
|
+
- auth_credentials.yml.example
|
96
111
|
- lib/salesforce_bulk_api.rb
|
112
|
+
- lib/salesforce_bulk_api/concerns/throttling.rb
|
97
113
|
- lib/salesforce_bulk_api/connection.rb
|
98
114
|
- lib/salesforce_bulk_api/job.rb
|
99
115
|
- lib/salesforce_bulk_api/version.rb
|
@@ -109,17 +125,16 @@ require_paths:
|
|
109
125
|
- lib
|
110
126
|
required_ruby_version: !ruby/object:Gem::Requirement
|
111
127
|
requirements:
|
112
|
-
- -
|
128
|
+
- - ">="
|
113
129
|
- !ruby/object:Gem::Version
|
114
130
|
version: '0'
|
115
131
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
132
|
requirements:
|
117
|
-
- -
|
133
|
+
- - ">="
|
118
134
|
- !ruby/object:Gem::Version
|
119
135
|
version: '0'
|
120
136
|
requirements: []
|
121
|
-
|
122
|
-
rubygems_version: 2.1.11
|
137
|
+
rubygems_version: 3.1.2
|
123
138
|
signing_key:
|
124
139
|
specification_version: 4
|
125
140
|
summary: It uses the bulk api of salesforce to communicate with Salesforce CRM
|