salesforce_bulk_api 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 97cff39898ba0d105b249e1e1ba27fdf05e9a518
4
- data.tar.gz: b4765152d9a544e7b80401148ff35ed789da0345
3
+ metadata.gz: e63ba9f2d9be3dfa89bf6d896ee94fe847bc1b57
4
+ data.tar.gz: e054528cb047b09383e98fe2d54b1e3ad628bb0e
5
5
  SHA512:
6
- metadata.gz: 13bf15920245ba4187fb5550249884d04537427c3c849194daf3b867cc1c39f9f3a7f3b7a3ee959b4ff0ba31368a9fa5e0ca75314b126c8097d598aa1cb9e9f6
7
- data.tar.gz: 3396900591b0ccf7a1d2759ee9a52168063bda97716741f993b5cc7979f182c046cad34bff9d38e193f538e5201a4592b6121d0aba0b1bf89a6b4eef4ebc5c6b
6
+ metadata.gz: 7d66df90281412848aba6eed1a5a5476d7883d1e774f28b21dcd2d3a3952027a9e527a474ec2cd702e5b1018c678a25892572206524d1cfbcbd064e7a7577495
7
+ data.tar.gz: 0326e9e4f4389d5052ce92aa04ff2daf65c26df997dc31d8b353f3cca60f1eb5634da45d126fb2db2d3221873353cd78df6ca8bab55d43f14199dd0d0de71c33
data/.gitignore CHANGED
@@ -1,4 +1,7 @@
1
1
  *.gem
2
2
  .bundle
3
3
  Gemfile.lock
4
+ .ruby-gemset
5
+ .ruby-version
4
6
  pkg/*
7
+ auth_credentials.yml
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Salesforce-Bulk-Api
2
+ [![Gem Version](https://badge.fury.io/rb/salesforce_bulk_api.png)](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`
14
+
15
+ or add
16
+
17
+ `gem salesforce_bulk_api`
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'
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'
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
+
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
+ ## Installation
86
+
87
+ sudo gem install salesforce_bulk_api
88
+
89
+ ## Contribute
90
+
91
+ Feel to fork and send Pull request
data/Rakefile CHANGED
@@ -1 +1,3 @@
1
- require "bundler/gem_tasks"
1
+ require 'rspec/core/rake_task'
2
+ task :default => :spec
3
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,5 @@
1
+ salesforce:
2
+ client_id: client_id_here
3
+ client_secret: client_secret_here
4
+ user: sf_user@example.com
5
+ passwordandtoken: passandtokenhere
@@ -1,4 +1,5 @@
1
1
  module SalesforceBulkApi
2
+ require 'timeout'
2
3
 
3
4
  class Connection
4
5
 
@@ -22,12 +23,17 @@ module SalesforceBulkApi
22
23
  #private
23
24
 
24
25
  def login()
25
- @session_id=@client.oauth_token
26
- @server_url=@client.instance_url
26
+ client_type = @client.class.to_s
27
+ case client_type
28
+ when "Restforce::Data::Client"
29
+ @session_id=@client.options[:oauth_token]
30
+ @server_url=@client.options[:instance_url]
31
+ else
32
+ @session_id=@client.oauth_token
33
+ @server_url=@client.instance_url
34
+ end
27
35
  @instance = parse_instance()
28
- puts @instance
29
36
  @@INSTANCE_HOST = "#{@instance}.salesforce.com"
30
- puts @@INSTANCE_HOST
31
37
  end
32
38
 
33
39
  def post_xml(host, path, xml, headers)
@@ -36,7 +42,19 @@ module SalesforceBulkApi
36
42
  headers['X-SFDC-Session'] = @session_id;
37
43
  path = "#{@@PATH_PREFIX}#{path}"
38
44
  end
39
- https(host).post(path, xml, headers).body
45
+ i = 0
46
+ begin
47
+ https(host).post(path, xml, headers).body
48
+ rescue
49
+ i += 1
50
+ if i < 3
51
+ puts "Request fail #{i}: Retrying #{path}"
52
+ retry
53
+ else
54
+ puts "FATAL: Request to #{path} failed three times."
55
+ raise
56
+ end
57
+ end
40
58
  end
41
59
 
42
60
  def get_request(host, path, headers)
@@ -57,6 +75,8 @@ module SalesforceBulkApi
57
75
 
58
76
  def parse_instance()
59
77
  @instance=@server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}/).to_s.gsub("https://","")
78
+ @instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.blank?
79
+ return @instance
60
80
  end
61
81
 
62
82
  end
@@ -3,22 +3,25 @@ module SalesforceBulkApi
3
3
  class Job
4
4
 
5
5
  def initialize(operation, sobject, records, external_field, connection)
6
-
7
- @@operation = operation
8
- @@sobject = sobject
9
- @@external_field = external_field
10
- @@records = records
11
- @@connection = connection
12
- @@XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>'
13
-
6
+ @operation = operation
7
+ @sobject = sobject
8
+ @external_field = external_field
9
+ @records = records
10
+ @connection = connection
11
+ @batch_ids = []
12
+ @XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>'
14
13
  end
15
14
 
16
- def create_job()
17
- xml = "#{@@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
18
- xml += "<operation>#{@@operation}</operation>"
19
- xml += "<object>#{@@sobject}</object>"
20
- if !@@external_field.nil? # This only happens on upsert
21
- xml += "<externalIdFieldName>#{@@external_field}</externalIdFieldName>"
15
+ def create_job(batch_size, send_nulls, no_null_list)
16
+ @batch_size = batch_size
17
+ @send_nulls = send_nulls
18
+ @no_null_list = no_null_list
19
+
20
+ xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
21
+ xml += "<operation>#{@operation}</operation>"
22
+ xml += "<object>#{@sobject}</object>"
23
+ if !@external_field.nil? # This only happens on upsert
24
+ xml += "<externalIdFieldName>#{@external_field}</externalIdFieldName>"
22
25
  end
23
26
  xml += "<contentType>XML</contentType>"
24
27
  xml += "</jobInfo>"
@@ -26,103 +29,169 @@ module SalesforceBulkApi
26
29
  path = "job"
27
30
  headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
28
31
 
29
- response = @@connection.post_xml(nil, path, xml, headers)
32
+ response = @connection.post_xml(nil, path, xml, headers)
30
33
  response_parsed = XmlSimple.xml_in(response)
31
- puts response
32
- @@job_id = response_parsed['id'][0]
34
+ @job_id = response_parsed['id'][0]
33
35
  end
34
36
 
35
37
  def close_job()
36
- xml = "#{@@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
38
+ xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
37
39
  xml += "<state>Closed</state>"
38
40
  xml += "</jobInfo>"
39
41
 
40
- path = "job/#{@@job_id}"
42
+ path = "job/#{@job_id}"
41
43
  headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
42
44
 
43
- response = @@connection.post_xml(nil, path, xml, headers)
44
- response_parsed = XmlSimple.xml_in(response)
45
-
46
- #job_id = response_parsed['id'][0]
45
+ response = @connection.post_xml(nil, path, xml, headers)
46
+ XmlSimple.xml_in(response)
47
47
  end
48
48
 
49
49
  def add_query
50
- path = "job/#{@@job_id}/batch/"
50
+ path = "job/#{@job_id}/batch/"
51
51
  headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
52
52
 
53
- response = @@connection.post_xml(nil, path, @@records, headers)
53
+ response = @connection.post_xml(nil, path, @records, headers)
54
54
  response_parsed = XmlSimple.xml_in(response)
55
55
 
56
- @@batch_id = response_parsed['id'][0]
56
+ @batch_ids << response_parsed['id'][0]
57
57
  end
58
58
 
59
- def add_batch()
60
- keys = @@records.reduce({}) {|h,pairs| pairs.each {|k,v| (h[k] ||= []) << v}; h}.keys
61
- headers = keys
62
- @@records_dup=@@records.clone
63
- super_records=[]
64
- (@@records_dup.size/10000).times do
65
- super_records<<@@records_dup.pop(10000)
59
+ def add_batches
60
+ 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
62
+
63
+ @records_dup = @records.clone
64
+
65
+ super_records = []
66
+ (@records_dup.size/@batch_size).to_i.times do
67
+ super_records << @records_dup.pop(@batch_size)
68
+ end
69
+ super_records << @records_dup unless @records_dup.empty?
70
+
71
+ super_records.each do |batch|
72
+ @batch_ids << add_batch(keys, batch)
66
73
  end
67
- super_records<<@@records_dup
68
-
69
- super_records.each do|batch|
70
- xml = "#{@@XML_HEADER}<sObjects xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
71
- batch.each do |r|
72
- xml += "<sObject>"
73
- keys.each do |k|
74
- xml += "<#{k}>#{r[k]}</#{k}>"
74
+ end
75
+
76
+ def add_batch(keys, batch)
77
+ xml = "#{@XML_HEADER}<sObjects xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
78
+ batch.each do |r|
79
+ xml += create_sobject(keys, r)
80
+ end
81
+ xml += '</sObjects>'
82
+ path = "job/#{@job_id}/batch/"
83
+ headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
84
+ response = @connection.post_xml(nil, path, xml, headers)
85
+ response_parsed = XmlSimple.xml_in(response)
86
+ response_parsed['id'][0] if response_parsed['id']
87
+ end
88
+
89
+ def create_sobject(keys, r)
90
+ sobject_xml = '<sObject>'
91
+ keys.each do |k|
92
+ if !r[k].to_s.empty?
93
+ sobject_xml += "<#{k}>"
94
+ if r[k].respond_to?(:encode)
95
+ sobject_xml += r[k].encode(:xml => :text)
96
+ else
97
+ sobject_xml += r[k].to_s
75
98
  end
76
- xml += "</sObject>"
99
+ sobject_xml += "</#{k}>"
100
+ elsif @send_nulls && !@no_null_list.include?(k)
101
+ sobject_xml += "<#{k} xsi:nil=\"true\"/>"
77
102
  end
78
- xml += "</sObjects>"
79
-
103
+ end
104
+ sobject_xml += '</sObject>'
105
+ sobject_xml
106
+ end
80
107
 
81
- path = "job/#{@@job_id}/batch/"
82
- headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
83
- response = @@connection.post_xml(nil, path, xml, headers)
84
- response_parsed = XmlSimple.xml_in(response)
108
+ def check_job_status
109
+ path = "job/#{@job_id}"
110
+ headers = Hash.new
111
+ response = @connection.get_request(nil, path, headers)
85
112
 
86
- @@batch_id = response_parsed['id'][0]
87
- puts @@batch_id
113
+ begin
114
+ response_parsed = XmlSimple.xml_in(response) if response
115
+ response_parsed
116
+ rescue StandardError => e
117
+ puts "Error parsing XML response for #{@job_id}"
118
+ puts e
119
+ puts e.backtrace
88
120
  end
89
121
  end
90
122
 
91
- def check_batch_status()
92
- path = "job/#{@@job_id}/batch/#{@@batch_id}"
123
+ def check_batch_status(batch_id)
124
+ path = "job/#{@job_id}/batch/#{batch_id}"
93
125
  headers = Hash.new
94
126
 
95
- response = @@connection.get_request(nil, path, headers)
96
- response_parsed = XmlSimple.xml_in(response)
127
+ response = @connection.get_request(nil, path, headers)
97
128
 
98
- # puts response_parsed
99
129
  begin
100
- #puts "check: #{response_parsed.inspect}\n"
130
+ response_parsed = XmlSimple.xml_in(response) if response
101
131
  response_parsed
102
- rescue Exception => e
103
- #puts "check: #{response_parsed.inspect}\n"
104
- nil
132
+ rescue StandardError => e
133
+ puts "Error parsing XML response for #{@job_id}, batch #{batch_id}"
134
+ puts e
135
+ puts e.backtrace
105
136
  end
106
137
  end
107
138
 
108
- def get_batch_result()
109
- path = "job/#{@@job_id}/batch/#{@@batch_id}/result"
139
+ def get_job_result(return_result, timeout)
140
+ # timeout is in seconds
141
+ begin
142
+ state = []
143
+ Timeout::timeout(timeout, SalesforceBulkApi::JobTimeout) do
144
+ while true
145
+ if self.check_job_status['state'][0] == 'Closed'
146
+ @batch_ids.each do |batch_id|
147
+ batch_state = self.check_batch_status(batch_id)
148
+ if batch_state['state'][0] != "Queued" && batch_state['state'][0] != "InProgress"
149
+ state << (batch_state)
150
+ @batch_ids.delete(batch_id)
151
+ end
152
+ end
153
+ break if @batch_ids.empty?
154
+ else
155
+ break
156
+ end
157
+ end
158
+ end
159
+ rescue SalesforceBulkApi::JobTimeout => e
160
+ puts 'Timeout waiting for Salesforce to process job batches #{@batch_ids} of job #{@job_id}.'
161
+ puts e
162
+ raise
163
+ end
164
+
165
+ state.each_with_index do |batch_state, i|
166
+ if batch_state['state'][0] == 'Completed' && return_result == true
167
+ state[i].merge!({'response' => self.get_batch_result(batch_state['id'][0])})
168
+ end
169
+ end
170
+ state
171
+ end
172
+
173
+ def get_batch_result(batch_id)
174
+ path = "job/#{@job_id}/batch/#{batch_id}/result"
110
175
  headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
111
176
 
112
- response = @@connection.get_request(nil, path, headers)
177
+ response = @connection.get_request(nil, path, headers)
178
+ response_parsed = XmlSimple.xml_in(response)
179
+ results = response_parsed['result'] unless @operation == 'query'
113
180
 
114
- if(@@operation == "query") # The query op requires us to do another request to get the results
115
- response_parsed = XmlSimple.xml_in(response)
181
+ if(@operation == 'query') # The query op requires us to do another request to get the results
116
182
  result_id = response_parsed["result"][0]
117
-
118
- path = "job/#{@@job_id}/batch/#{@@batch_id}/result/#{result_id}"
183
+ path = "job/#{@job_id}/batch/#{batch_id}/result/#{result_id}"
119
184
  headers = Hash.new
120
185
  headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
121
- response = @@connection.get_request(nil, path, headers)
186
+ response = @connection.get_request(nil, path, headers)
187
+ response_parsed = XmlSimple.xml_in(response)
188
+ results = response_parsed['records']
122
189
  end
123
-
124
-
190
+ results
125
191
  end
126
192
 
127
193
  end
194
+
195
+ class JobTimeout < StandardError
196
+ end
128
197
  end
@@ -1,3 +1,3 @@
1
1
  module SalesforceBulkApi
2
- VERSION = "0.0.6"
2
+ VERSION = '0.0.8'
3
3
  end
@@ -1,14 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require()
1
4
  require "salesforce_bulk_api/version"
2
5
  require 'net/https'
3
- require 'rubygems'
4
6
  require 'xmlsimple'
5
7
  require 'csv'
6
- require "salesforce_bulk_api/version"
7
8
  require 'salesforce_bulk_api/job'
8
9
  require 'salesforce_bulk_api/connection'
9
10
 
10
11
  module SalesforceBulkApi
11
- # Your code goes here...
12
+
12
13
  class Api
13
14
 
14
15
  @@SALESFORCE_API_VERSION = '23.0'
@@ -17,50 +18,36 @@ module SalesforceBulkApi
17
18
  @connection = SalesforceBulkApi::Connection.new(@@SALESFORCE_API_VERSION,client)
18
19
  end
19
20
 
20
- def upsert(sobject, records, external_field)
21
- self.do_operation('upsert', sobject, records, external_field)
21
+ def upsert(sobject, records, external_field, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500)
22
+ self.do_operation('upsert', sobject, records, external_field, get_response, timeout, batch_size, send_nulls, no_null_list)
22
23
  end
23
24
 
24
- def update(sobject, records)
25
- self.do_operation('update', sobject, records, nil)
25
+ def update(sobject, records, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500)
26
+ self.do_operation('update', sobject, records, nil, get_response, timeout, batch_size, send_nulls, no_null_list)
26
27
  end
27
28
 
28
- def create(sobject, records)
29
- self.do_operation('insert', sobject, records, nil)
29
+ def create(sobject, records, get_response = false, send_nulls = false, batch_size = 10000, timeout = 1500)
30
+ self.do_operation('insert', sobject, records, nil, get_response, timeout, batch_size, send_nulls)
30
31
  end
31
32
 
32
- def delete(sobject, records)
33
- self.do_operation('delete', sobject, records, nil)
33
+ def delete(sobject, records, get_response = false, batch_size = 10000, timeout = 1500)
34
+ self.do_operation('delete', sobject, records, nil, get_response, timeout, batch_size)
34
35
  end
35
36
 
36
- def query(sobject, query)
37
- self.do_operation('query', sobject, query, nil)
37
+ def query(sobject, query, batch_size = 10000, timeout = 1500)
38
+ self.do_operation('query', sobject, query, nil, true, timeout, batch_size)
38
39
  end
39
40
 
40
41
  #private
41
42
 
42
- def do_operation(operation, sobject, records, external_field)
43
+ def do_operation(operation, sobject, records, external_field, get_response, timeout, batch_size, send_nulls = false, no_null_list = [])
43
44
  job = SalesforceBulkApi::Job.new(operation, sobject, records, external_field, @connection)
44
45
 
45
- # TODO: put this in one function
46
- job_id = job.create_job()
47
- batch_id = operation == "query" ? job.add_query() : job.add_batch()
48
- job.close_job()
49
-
50
- while true
51
- state = job.check_batch_status()
52
- if state['state'][0] != "Queued" && state['state'][0] != "InProgress"
53
- break
54
- end
55
- sleep(2) # wait x seconds and check again
56
- end
57
-
58
- if state['state'][0] == 'Completed'
59
- job.get_batch_result()
60
- return state
61
- else
62
- return "error"
63
- end
46
+ job.create_job(batch_size, send_nulls, no_null_list)
47
+ operation == "query" ? job.add_query() : job.add_batches()
48
+ response = job.close_job
49
+ response.merge!({'batches' => job.get_job_result(get_response, timeout)}) if get_response == true
50
+ response
64
51
  end
65
- end # End class
66
- end
52
+ end
53
+ end
@@ -7,21 +7,23 @@ Gem::Specification.new do |s|
7
7
  s.version = SalesforceBulkApi::VERSION
8
8
  s.authors = ["Yatish Mehta"]
9
9
  s.email = ["yatishmehta27@gmail.com"]
10
+
10
11
  s.homepage = "https://github.com/yatishmehta27/salesforce_bulk_api"
11
- s.summary = %q{It uses the bulk api of salesforce to communicate with teh salesforce CRM}
12
+ s.summary = %q{It uses the bulk api of salesforce to communicate with Salesforce CRM}
12
13
  s.description = %q{Salesforce Bulk API with governor limits taken care of}
13
14
 
14
15
  s.rubyforge_project = "salesforce_bulk_api"
15
- s.add_dependency(%q<oauth2>, [">= 0.9.1"])
16
- s.add_dependency(%q<databasedotcom>, [">= 0"])
16
+
17
17
  s.add_dependency(%q<json>, [">= 0"])
18
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
+
19
24
  s.files = `git ls-files`.split("\n")
20
25
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
26
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
27
  s.require_paths = ["lib"]
23
28
 
24
- # specify any dependencies here; for example:
25
- # s.add_development_dependency "rspec"
26
- # s.add_runtime_dependency "rest-client"
27
29
  end
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+ require 'yaml'
3
+ require 'databasedotcom'
4
+
5
+ describe SalesforceBulkApi do
6
+
7
+ before :each do
8
+ auth_hash = YAML.load(File.read('auth_credentials.yml'))
9
+ @sf_client = Databasedotcom::Client.new(:client_id => auth_hash['salesforce']['client_id'],
10
+ :client_secret => auth_hash['salesforce']['client_secret'])
11
+ @sf_client.authenticate(:username => auth_hash['salesforce']['user'], :password => auth_hash['salesforce']['passwordandtoken'])
12
+ @api = SalesforceBulkApi::Api.new(@sf_client)
13
+ end
14
+
15
+ describe 'upsert' do
16
+
17
+ context 'when not passed get_result' do
18
+ it "doesn't return the batches array" do
19
+ res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}], 'Id')
20
+ res['batches'].should be_nil
21
+ end
22
+ end
23
+
24
+ context 'when passed get_result = true' do
25
+ it 'returns the batches array' do
26
+ res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}], 'Id', true)
27
+ res['batches'][0]['response'].is_a? Array
28
+ res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
29
+ end
30
+ end
31
+
32
+ context 'when passed send_nulls = true' do
33
+ it 'sets the nil and empty attributes to NULL' do
34
+ @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'abc123', :Other_Phone__c => '5678', :Gold_Star__c => true}], true)
35
+ res = @api.query('Account', "SELECT Website, Other_Phone__c From Account WHERE Id = '0013000000ymMBh'")
36
+ res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
37
+ res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq '5678'
38
+ res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => '', :Other_Phone__c => nil, :Gold_Star__c => false, :CRM_Last_Modified__c => nil}], 'Id', true, true)
39
+ res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
40
+ res = @api.query('Account', "SELECT Website, Other_Phone__c, Gold_Star__c, CRM_Last_Modified__c From Account WHERE Id = '0013000000ymMBh'")
41
+ res['batches'][0]['response'][0]['Website'][0].should eq({"xsi:nil" => "true"})
42
+ res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq({"xsi:nil" => "true"})
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"})
45
+ end
46
+ end
47
+
48
+ context 'when passed send_nulls = true and an array of fields not to null' do
49
+ 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 => '0013000000ymMBh', :Website => 'abc123', :Other_Phone__c => '5678', :Gold_Star__c => true}], true)
51
+ res = @api.query('Account', "SELECT Website, Other_Phone__c From Account WHERE Id = '0013000000ymMBh'")
52
+ res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
53
+ res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq '5678'
54
+ res = @api.upsert('Account', [{:Id => '0013000000ymMBh', :Website => '', :Other_Phone__c => nil, :Gold_Star__c => false, :CRM_Last_Modified__c => nil}], 'Id', true, true, [:Website, :Other_Phone__c])
55
+ res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
56
+ res = @api.query('Account', "SELECT Website, Other_Phone__c, Gold_Star__c, CRM_Last_Modified__c From Account WHERE Id = '0013000000ymMBh'")
57
+ res['batches'][0]['response'][0]['Website'][0].should eq('abc123')
58
+ res['batches'][0]['response'][0]['Other_Phone__c'][0].should eq('5678')
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"})
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ describe 'update' do
67
+ context 'when there is not an error' do
68
+ context 'when not passed get_result' do
69
+ it "doesnt return the batches array" do
70
+ res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}])
71
+ res['batches'].should be_nil
72
+ end
73
+ end
74
+
75
+ context 'when passed get_result = true' do
76
+ it 'returns the batches array' do
77
+ res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}], true)
78
+ res['batches'][0]['response'].is_a? Array
79
+ res['batches'][0]['response'][0].should eq({'id'=>['0013000000ymMBhAAM'], 'success'=>['true'], 'created'=>['false']})
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'when there is an error' do
85
+ context 'when not passed get_result' do
86
+ it "doesn't return the results array" do
87
+ res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'},{:Id => 'abc123', :Website => 'www.test.com'}])
88
+ res['batches'].should be_nil
89
+ end
90
+ end
91
+
92
+ context 'when passed get_result = true with batches' do
93
+ it 'returns the results array' do
94
+ res = @api.update('Account', [{:Id => '0013000000ymMBh', :Website => 'www.test.com'}, {:Id => '0013000000ymMBh', :Website => 'www.test.com'}, {:Id => '0013000000ymMBh', :Website => 'www.test.com'}, {:Id => 'abc123', :Website => 'www.test.com'}], true, false, [], 2)
95
+ res['batches'][0]['response'].should eq([{"id"=>["0013000000ymMBhAAM"], "success"=>["true"], "created"=>["false"]}, {"errors"=>[{"fields"=>["Id"], "message"=>["Account ID: id value of incorrect type: abc123"], "statusCode"=>["MALFORMED_ID"]}], "success"=>["false"], "created"=>["false"]}])
96
+ res['batches'][1]['response'].should eq([{"id"=>["0013000000ymMBhAAM"], "success"=>["true"], "created"=>["false"]},{"id"=>["0013000000ymMBhAAM"], "success"=>["true"], "created"=>["false"]}])
97
+ end
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ describe 'create' do
104
+ pending
105
+ end
106
+
107
+ describe 'delete' do
108
+ pending
109
+ end
110
+
111
+ describe 'query' do
112
+
113
+ context 'when there are results' do
114
+ it 'returns the query results' do
115
+ res = @api.query('Account', "SELECT id, Name From Account WHERE Name LIKE 'Test%'")
116
+ res['batches'][0]['response'].length.should > 1
117
+ res['batches'][0]['response'][0]['Id'].should_not be_nil
118
+ end
119
+ context 'and there are multiple batches' do
120
+ it 'returns the query results in a merged hash' do
121
+ pending 'need dev to create > 10k records in dev organization'
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
126
+ end
127
+ end
128
+
129
+ context 'when there are no results' do
130
+ it 'returns nil' do
131
+ res = @api.query('Account', "SELECT id From Account WHERE Name = 'ABC'")
132
+ res['batches'][0]['response'].should eq nil
133
+ end
134
+ end
135
+
136
+ context 'when there is an error' do
137
+ it 'returns nil' do
138
+ res = @api.query('Account', "SELECT id From Account WHERE Name = ''ABC'")
139
+ res['batches'][0]['response'].should eq nil
140
+ end
141
+ end
142
+
143
+ end
144
+
145
+ end
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ #require 'webmock/rspec'
4
+ #require 'vcr'
5
+ require 'salesforce_bulk_api'
6
+
7
+ RSpec.configure do |c|
8
+ c.filter_run :focus => true
9
+ c.run_all_when_everything_filtered = true
10
+ 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,31 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: salesforce_bulk_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yatish Mehta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-03-16 00:00:00.000000000 Z
11
+ date: 2014-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: oauth2
14
+ name: json
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - '>='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.9.1
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
- version: 0.9.1
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: databasedotcom
28
+ name: xml-simple
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - '>='
@@ -39,13 +39,13 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: json
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
- type: :runtime
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
@@ -53,19 +53,33 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: xml-simple
56
+ name: webmock
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - '>='
59
+ - - ~>
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :runtime
61
+ version: '1.13'
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: '0'
68
+ version: '1.13'
69
+ - !ruby/object:Gem::Dependency
70
+ name: vcr
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '2.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '2.5'
69
83
  description: Salesforce Bulk API with governor limits taken care of
70
84
  email:
71
85
  - yatishmehta27@gmail.com
@@ -74,14 +88,18 @@ extensions: []
74
88
  extra_rdoc_files: []
75
89
  files:
76
90
  - .gitignore
91
+ - .rspec
77
92
  - Gemfile
78
- - README.rdoc
93
+ - README.md
79
94
  - Rakefile
95
+ - example_auth_credentials.yml
80
96
  - lib/salesforce_bulk_api.rb
81
97
  - lib/salesforce_bulk_api/connection.rb
82
98
  - lib/salesforce_bulk_api/job.rb
83
99
  - lib/salesforce_bulk_api/version.rb
84
100
  - salesforce_bulk_api.gemspec
101
+ - spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb
102
+ - spec/spec_helper.rb
85
103
  homepage: https://github.com/yatishmehta27/salesforce_bulk_api
86
104
  licenses: []
87
105
  metadata: {}
@@ -101,8 +119,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
119
  version: '0'
102
120
  requirements: []
103
121
  rubyforge_project: salesforce_bulk_api
104
- rubygems_version: 2.0.0
122
+ rubygems_version: 2.1.11
105
123
  signing_key:
106
124
  specification_version: 4
107
- summary: It uses the bulk api of salesforce to communicate with teh salesforce CRM
108
- test_files: []
125
+ summary: It uses the bulk api of salesforce to communicate with Salesforce CRM
126
+ test_files:
127
+ - spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb
128
+ - spec/spec_helper.rb
data/README.rdoc DELETED
@@ -1,66 +0,0 @@
1
- = Salesforce-Bulk-Api
2
-
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
- sudo gem install salesforce_bulk_api
13
- or add
14
- gem salesforce_bulk_api
15
- in your Gemfile
16
-
17
-
18
- Please do check the entire documentation of the databasedotcom gem and it various ways of authentication
19
- Databasedotcom[https://github.com/heroku/databasedotcom]
20
-
21
- You can use username password combo, OmniAuth, Oauth2
22
- You can as many records possible in the Array. Governor limits are taken care of inside the gem.
23
-
24
-
25
-
26
- require 'salesforce_bulk_api'
27
- client = Databasedotcom::Client.new :client_id => $SFDC_APP_CONFIG["client_id"], :client_secret => $SFDC_APP_CONFIG["client_secret"] #client_id and client_secret respectively
28
- client.authenticate :token => "my-oauth-token", :instance_url => "http://na1.salesforce.com" #=> "my-oauth-token"
29
- salesforce = SalesforceBulkApi::Api.new(client)
30
-
31
- Sample operations:
32
-
33
- # Insert/Create
34
- new_account = Hash["name" => "Test Account", "type" => "Other"] # Add as many fields per record as needed.
35
- records_to_insert = Array.new
36
- records_to_insert.push(new_account) # You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
37
- result = salesforce.create("Account", records_to_insert)
38
- puts "result is: #{result.inspect}"
39
-
40
- # Update
41
- updated_account = Hash["name" => "Test Account -- Updated", id => "a00A0001009zA2m"] # Nearly identical to an insert, but we need to pass the salesforce id.
42
- records_to_update = Array.new
43
- records_to_update.push(updated_account)
44
- salesforce.update("Account", records_to_update)
45
-
46
- # Upsert
47
- upserted_account = Hash["name" => "Test Account -- Upserted", "External_Field_Name" => "123456"] # Fields to be updated. External field must be included
48
- records_to_upsert = Array.new
49
- records_to_upsert.push(upserted_account)
50
- salesforce.upsert("Account", records_to_upsert, "External_Field_Name") # Note that upsert accepts an extra parameter for the external field name
51
-
52
- # Delete
53
- deleted_account = Hash["id" => "a00A0001009zA2m"] # We only specify the id of the records to delete
54
- records_to_delete = Array.new
55
- records_to_delete.push(deleted_account)
56
- salesforce.delete("Account", records_to_delete)
57
-
58
- # Query
59
- res = salesforce.query("Account", "select id, name, createddate from Account limit 3") # We just need to pass the sobject name and the query string
60
-
61
- ==Installation
62
- sudo gem install salesforce_bulk_api
63
-
64
- == Copyright
65
-
66
- Copyright (c) 2012 Yatish Mehta