salesforce_bulk_api 0.0.12 → 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 +1 -0
- data/LICENCE +21 -0
- data/README.md +99 -70
- data/{example_auth_credentials.yml → auth_credentials.yml.example} +0 -0
- data/lib/salesforce_bulk_api.rb +13 -8
- data/lib/salesforce_bulk_api/concerns/throttling.rb +2 -2
- data/lib/salesforce_bulk_api/connection.rb +23 -30
- data/lib/salesforce_bulk_api/job.rb +49 -11
- data/lib/salesforce_bulk_api/version.rb +1 -1
- data/salesforce_bulk_api.gemspec +2 -4
- data/spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb +5 -5
- metadata +11 -11
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,112 +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
|
-
|
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
|
+
```
|
52
65
|
|
53
66
|
### Sample operations:
|
54
67
|
|
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
|
-
|
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
|
+
```
|
84
99
|
|
85
100
|
### Helpful methods:
|
86
101
|
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
+
```
|
90
107
|
|
91
108
|
### Listening to events:
|
92
109
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
+
```
|
99
118
|
|
100
|
-
###
|
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"]}]
|
101
128
|
|
102
|
-
|
103
|
-
|
104
|
-
|
129
|
+
```
|
130
|
+
|
131
|
+
### Throttling API calls:
|
105
132
|
|
106
|
-
|
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
|
+
```
|
107
138
|
|
108
|
-
sudo gem install salesforce_bulk_api
|
109
|
-
|
110
139
|
## Contribute
|
111
140
|
|
112
141
|
Feel to fork and send Pull request
|
File without changes
|
data/lib/salesforce_bulk_api.rb
CHANGED
@@ -1,23 +1,22 @@
|
|
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
8
|
require 'salesforce_bulk_api/concerns/throttling'
|
9
9
|
require 'salesforce_bulk_api/job'
|
10
10
|
require 'salesforce_bulk_api/connection'
|
11
11
|
|
12
12
|
module SalesforceBulkApi
|
13
|
-
|
14
13
|
class Api
|
15
14
|
attr_reader :connection
|
16
15
|
|
17
|
-
|
16
|
+
SALESFORCE_API_VERSION = '46.0'
|
18
17
|
|
19
18
|
def initialize(client)
|
20
|
-
@connection = SalesforceBulkApi::Connection.new(
|
19
|
+
@connection = SalesforceBulkApi::Connection.new(SALESFORCE_API_VERSION, client)
|
21
20
|
@listeners = { job_created: [] }
|
22
21
|
end
|
23
22
|
|
@@ -53,12 +52,11 @@ module SalesforceBulkApi
|
|
53
52
|
}
|
54
53
|
end
|
55
54
|
|
56
|
-
|
57
|
-
##
|
58
55
|
# Allows you to attach a listener that accepts the created job (which has a useful #job_id field). This is useful
|
59
56
|
# for recording a job ID persistently before you begin batch work (i.e. start modifying the salesforce database),
|
60
57
|
# so if the load process you are writing needs to recover, it can be aware of previous jobs it started and wait
|
61
58
|
# for them to finish.
|
59
|
+
#
|
62
60
|
def on_job_created(&block)
|
63
61
|
@listeners[:job_created] << block
|
64
62
|
end
|
@@ -70,7 +68,13 @@ module SalesforceBulkApi
|
|
70
68
|
def do_operation(operation, sobject, records, external_field, get_response, timeout, batch_size, send_nulls = false, no_null_list = [])
|
71
69
|
count operation.to_sym
|
72
70
|
|
73
|
-
job = SalesforceBulkApi::Job.new(
|
71
|
+
job = SalesforceBulkApi::Job.new(
|
72
|
+
operation: operation,
|
73
|
+
sobject: sobject,
|
74
|
+
records: records,
|
75
|
+
external_field: external_field,
|
76
|
+
connection: @connection
|
77
|
+
)
|
74
78
|
|
75
79
|
job.create_job(batch_size, send_nulls, no_null_list)
|
76
80
|
@listeners[:job_created].each {|callback| callback.call(job)}
|
@@ -81,6 +85,7 @@ module SalesforceBulkApi
|
|
81
85
|
end
|
82
86
|
|
83
87
|
private
|
88
|
+
|
84
89
|
def get_counters
|
85
90
|
@counters ||= Hash.new(0)
|
86
91
|
end
|
@@ -11,7 +11,7 @@ module SalesforceBulkApi::Concerns
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def set_status_throttle(limit_seconds)
|
14
|
-
set_throttle_limit_in_seconds(limit_seconds, [:http_method, :path], ->(details) { details[:
|
14
|
+
set_throttle_limit_in_seconds(limit_seconds, [:http_method, :path], ->(details) { details[:http_method] == :get })
|
15
15
|
end
|
16
16
|
|
17
17
|
def set_throttle_limit_in_seconds(limit_seconds, throttle_by_keys, only_if)
|
@@ -57,4 +57,4 @@ module SalesforceBulkApi::Concerns
|
|
57
57
|
end
|
58
58
|
|
59
59
|
end
|
60
|
-
end
|
60
|
+
end
|
@@ -1,22 +1,15 @@
|
|
1
|
-
module SalesforceBulkApi
|
2
1
|
require 'timeout'
|
3
2
|
|
3
|
+
module SalesforceBulkApi
|
4
4
|
class Connection
|
5
5
|
include Concerns::Throttling
|
6
6
|
|
7
|
-
|
8
|
-
@@API_VERSION = nil
|
9
|
-
@@LOGIN_HOST = 'login.salesforce.com'
|
10
|
-
@@INSTANCE_HOST = nil # Gets set in login()
|
7
|
+
LOGIN_HOST = 'login.salesforce.com'
|
11
8
|
|
12
|
-
def initialize(api_version,client)
|
13
|
-
@client=client
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@instance = nil
|
17
|
-
@@API_VERSION = api_version
|
18
|
-
@@LOGIN_PATH = "/services/Soap/u/#{@@API_VERSION}"
|
19
|
-
@@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}/"
|
20
13
|
|
21
14
|
login()
|
22
15
|
end
|
@@ -25,21 +18,21 @@ require 'timeout'
|
|
25
18
|
client_type = @client.class.to_s
|
26
19
|
case client_type
|
27
20
|
when "Restforce::Data::Client"
|
28
|
-
@session_id
|
29
|
-
@server_url
|
21
|
+
@session_id = @client.options[:oauth_token]
|
22
|
+
@server_url = @client.options[:instance_url]
|
30
23
|
else
|
31
|
-
@session_id
|
32
|
-
@server_url
|
24
|
+
@session_id = @client.oauth_token
|
25
|
+
@server_url = @client.instance_url
|
33
26
|
end
|
34
27
|
@instance = parse_instance()
|
35
|
-
|
28
|
+
@instance_host = "#{@instance}.salesforce.com"
|
36
29
|
end
|
37
30
|
|
38
31
|
def post_xml(host, path, xml, headers)
|
39
|
-
host = host ||
|
40
|
-
if host !=
|
32
|
+
host = host || @instance_host
|
33
|
+
if host != LOGIN_HOST # Not login, need to add session id to header
|
41
34
|
headers['X-SFDC-Session'] = @session_id
|
42
|
-
path = "#{
|
35
|
+
path = "#{@path_prefix}#{path}"
|
43
36
|
end
|
44
37
|
i = 0
|
45
38
|
begin
|
@@ -59,9 +52,9 @@ require 'timeout'
|
|
59
52
|
end
|
60
53
|
|
61
54
|
def get_request(host, path, headers)
|
62
|
-
host = host ||
|
63
|
-
path = "#{
|
64
|
-
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
|
65
58
|
headers['X-SFDC-Session'] = @session_id;
|
66
59
|
end
|
67
60
|
|
@@ -77,12 +70,6 @@ require 'timeout'
|
|
77
70
|
req
|
78
71
|
end
|
79
72
|
|
80
|
-
def parse_instance()
|
81
|
-
@instance = @server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}/).to_s.gsub("https://","")
|
82
|
-
@instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.nil? || @instance.empty?
|
83
|
-
return @instance
|
84
|
-
end
|
85
|
-
|
86
73
|
def counters
|
87
74
|
{
|
88
75
|
get: get_counters[:get],
|
@@ -100,6 +87,12 @@ require 'timeout'
|
|
100
87
|
get_counters[http_method] += 1
|
101
88
|
end
|
102
89
|
|
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
|
94
|
+
end
|
95
|
+
|
103
96
|
end
|
104
97
|
|
105
98
|
end
|
@@ -16,8 +16,6 @@ module SalesforceBulkApi
|
|
16
16
|
@XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>'
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
19
|
def create_job(batch_size, send_nulls, no_null_list)
|
22
20
|
@batch_size = batch_size
|
23
21
|
@send_nulls = send_nulls
|
@@ -26,7 +24,8 @@ module SalesforceBulkApi
|
|
26
24
|
xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
|
27
25
|
xml += "<operation>#{@operation}</operation>"
|
28
26
|
xml += "<object>#{@sobject}</object>"
|
29
|
-
|
27
|
+
# This only happens on upsert
|
28
|
+
if !@external_field.nil?
|
30
29
|
xml += "<externalIdFieldName>#{@external_field}</externalIdFieldName>"
|
31
30
|
end
|
32
31
|
xml += "<contentType>XML</contentType>"
|
@@ -42,7 +41,6 @@ module SalesforceBulkApi
|
|
42
41
|
raise SalesforceException.new("#{response_parsed['exceptionMessage'][0]} (#{response_parsed['exceptionCode'][0]})") if response_parsed['exceptionCode']
|
43
42
|
|
44
43
|
@job_id = response_parsed['id'][0]
|
45
|
-
|
46
44
|
end
|
47
45
|
|
48
46
|
def close_job()
|
@@ -74,7 +72,7 @@ module SalesforceBulkApi
|
|
74
72
|
@records_dup = @records.clone
|
75
73
|
|
76
74
|
super_records = []
|
77
|
-
(@records_dup.size
|
75
|
+
(@records_dup.size / @batch_size).to_i.times do
|
78
76
|
super_records << @records_dup.pop(@batch_size)
|
79
77
|
end
|
80
78
|
super_records << @records_dup unless @records_dup.empty?
|
@@ -102,6 +100,11 @@ module SalesforceBulkApi
|
|
102
100
|
data.keys.each do |k|
|
103
101
|
if k.is_a?(Hash)
|
104
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}>"
|
105
108
|
elsif data[k] != :type
|
106
109
|
xml += "<#{k}>#{data[k]}</#{k}>"
|
107
110
|
end
|
@@ -109,6 +112,21 @@ module SalesforceBulkApi
|
|
109
112
|
xml += '</sObject>'
|
110
113
|
end
|
111
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
|
+
|
112
130
|
def create_sobject(keys, r)
|
113
131
|
sobject_xml = '<sObject>'
|
114
132
|
keys.each do |k|
|
@@ -116,6 +134,8 @@ module SalesforceBulkApi
|
|
116
134
|
sobject_xml += "<#{k}>"
|
117
135
|
sobject_xml += build_sobject(r[k])
|
118
136
|
sobject_xml += "</#{k}>"
|
137
|
+
elsif k.to_s.include? '.'
|
138
|
+
sobject_xml += build_relationship_sobject(k, r[k])
|
119
139
|
elsif !r[k].to_s.empty?
|
120
140
|
sobject_xml += "<#{k}>"
|
121
141
|
if r[k].respond_to?(:encode)
|
@@ -126,7 +146,7 @@ module SalesforceBulkApi
|
|
126
146
|
sobject_xml += r[k].to_s
|
127
147
|
end
|
128
148
|
sobject_xml += "</#{k}>"
|
129
|
-
elsif @send_nulls && !@no_null_list.include?(k)
|
149
|
+
elsif @send_nulls && !@no_null_list.include?(k) && r.key?(k)
|
130
150
|
sobject_xml += "<#{k} xsi:nil=\"true\"/>"
|
131
151
|
end
|
132
152
|
end
|
@@ -171,11 +191,18 @@ module SalesforceBulkApi
|
|
171
191
|
state = []
|
172
192
|
Timeout::timeout(timeout, SalesforceBulkApi::JobTimeout) do
|
173
193
|
while true
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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])
|
179
206
|
@batch_ids.delete(batch_id)
|
180
207
|
end
|
181
208
|
end
|
@@ -219,6 +246,17 @@ module SalesforceBulkApi
|
|
219
246
|
results
|
220
247
|
end
|
221
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
|
+
|
222
260
|
end
|
223
261
|
|
224
262
|
class JobTimeout < StandardError
|
data/salesforce_bulk_api.gemspec
CHANGED
@@ -12,14 +12,12 @@ Gem::Specification.new do |s|
|
|
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.rubyforge_project = 'salesforce_bulk_api'
|
16
|
-
|
17
15
|
s.add_dependency('json', ['>= 0'])
|
18
16
|
s.add_dependency('xml-simple', ['>= 0'])
|
19
17
|
|
20
18
|
s.add_development_dependency 'rspec'
|
21
|
-
s.add_development_dependency 'restforce', '~>
|
22
|
-
s.add_development_dependency
|
19
|
+
s.add_development_dependency 'restforce', '~> 3.0.0'
|
20
|
+
s.add_development_dependency "rake", ">= 12.3.3"
|
23
21
|
s.add_development_dependency 'pry'
|
24
22
|
|
25
23
|
s.files = `git ls-files`.split("\n")
|
@@ -111,18 +111,18 @@ describe SalesforceBulkApi do
|
|
111
111
|
context 'when passed get_result = true with batches' do
|
112
112
|
it 'returns the results array' do
|
113
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
|
+
|
114
115
|
res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
|
115
116
|
res['batches'][0]['response'][0]['success'].should eq ['true']
|
116
117
|
res['batches'][0]['response'][0]['created'].should eq ['false']
|
117
|
-
|
118
|
-
res['batches'][0]['response'][1].should eq
|
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']
|
119
121
|
|
120
122
|
res['batches'][1]['response'][0]['id'][0].should start_with(@account_id)
|
121
123
|
res['batches'][1]['response'][0]['success'].should eq ['true']
|
122
124
|
res['batches'][1]['response'][0]['created'].should eq ['false']
|
123
|
-
res['batches'][1]['response'][1][
|
124
|
-
res['batches'][1]['response'][1]['success'].should eq ['true']
|
125
|
-
res['batches'][1]['response'][1]['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
126
|
end
|
127
127
|
end
|
128
128
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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
|
@@ -58,28 +58,28 @@ dependencies:
|
|
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
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
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: pry
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,9 +104,10 @@ files:
|
|
104
104
|
- ".gitignore"
|
105
105
|
- ".rspec"
|
106
106
|
- Gemfile
|
107
|
+
- LICENCE
|
107
108
|
- README.md
|
108
109
|
- Rakefile
|
109
|
-
-
|
110
|
+
- auth_credentials.yml.example
|
110
111
|
- lib/salesforce_bulk_api.rb
|
111
112
|
- lib/salesforce_bulk_api/concerns/throttling.rb
|
112
113
|
- lib/salesforce_bulk_api/connection.rb
|
@@ -133,8 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
134
|
- !ruby/object:Gem::Version
|
134
135
|
version: '0'
|
135
136
|
requirements: []
|
136
|
-
|
137
|
-
rubygems_version: 2.4.5
|
137
|
+
rubygems_version: 3.1.2
|
138
138
|
signing_key:
|
139
139
|
specification_version: 4
|
140
140
|
summary: It uses the bulk api of salesforce to communicate with Salesforce CRM
|