bulkforce 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ class Bulkforce
2
+ class Batch
3
+ attr_reader :job_id
4
+
5
+ def initialize connection, job_id, batch_id
6
+ @connection = connection
7
+ @job_id = job_id
8
+ @batch_id = batch_id
9
+
10
+ if @batch_id == -1
11
+ @final_status = {
12
+ state: "Completed",
13
+ state_message: "Empty Request"
14
+ }
15
+ end
16
+ end
17
+
18
+ def final_status poll_interval=2
19
+ return @final_status if @final_status
20
+
21
+ @final_status = self.status
22
+ while ["Queued", "InProgress"].include?(@final_status[:state])
23
+ sleep poll_interval
24
+ @final_status = self.status
25
+ yield @final_status if block_given?
26
+ end
27
+
28
+ raise @final_status[:state_message] if @final_status[:state] == "Failed"
29
+
30
+ @final_status.merge({
31
+ results: results
32
+ })
33
+ end
34
+
35
+ def status
36
+ @connection.query_batch @job_id, @batch_id
37
+ end
38
+
39
+ # results returned from Salesforce can be a single page id, or an array of ids.
40
+ # if it"s an array of ids, we will fetch the results from each, and concatenate them.
41
+ def results
42
+ Array(query_result_id).map do |result_id|
43
+ @connection.query_batch_result_data(@job_id, @batch_id, result_id)
44
+ end.flatten
45
+ end
46
+
47
+ def raw_request
48
+ @connection.raw_request
49
+ end
50
+
51
+ def raw_result
52
+ @connection.raw_result
53
+ end
54
+
55
+ private
56
+ def query_result_id
57
+ result_raw = @connection.query_batch_result_id(@job_id, @batch_id)
58
+ result_raw[:result] if result_raw
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ class Bulkforce
2
+ class Configuration
3
+ attr_accessor :api_version
4
+ attr_accessor :username
5
+ attr_accessor :password
6
+ attr_accessor :security_token
7
+ attr_accessor :host
8
+ attr_accessor :session_id
9
+ attr_accessor :instance
10
+ attr_accessor :client_id
11
+ attr_accessor :client_secret
12
+ attr_accessor :refresh_token
13
+
14
+ def initialize
15
+ @api_version = ENV["SALESFORCE_API_VERSION"] || "33.0"
16
+ @username = ENV["SALESFORCE_USERNAME"]
17
+ @password = ENV["SALESFORCE_PASSWORD"]
18
+ @security_token = ENV["SALESFORCE_SECURITY_TOKEN"]
19
+ @host = ENV["SALESFORCE_HOST"] || "login.salesforce.com"
20
+ @session_id = ENV["SALESFORCE_SESSION_ID"]
21
+ @instance = ENV["SALESFORCE_INSTANCE"]
22
+ @client_id = ENV["SALESFORCE_CLIENT_ID"]
23
+ @client_secret = ENV["SALESFORCE_CLIENT_SECRET"]
24
+ @refresh_token = ENV["SALESFORCE_REFRESH_TOKEN"]
25
+ end
26
+
27
+ def to_h
28
+ {
29
+ api_version: api_version,
30
+ username: username,
31
+ password: password,
32
+ security_token: security_token,
33
+ host: host,
34
+ session_id: session_id,
35
+ instance: instance,
36
+ client_id: client_id,
37
+ client_secret: client_secret,
38
+ refresh_token: refresh_token,
39
+ }.reject { |_, v| v.nil? }.to_h
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,114 @@
1
+ class Bulkforce
2
+ class Connection
3
+ attr_reader :raw_request
4
+ attr_reader :raw_result
5
+
6
+ def initialize(session_id:, instance:, api_version:)
7
+ @api_version = api_version
8
+ @session_id = session_id
9
+ @instance = instance
10
+ end
11
+
12
+ def login
13
+ response = Bulkforce::Http.login(
14
+ @host,
15
+ @username,
16
+ @password,
17
+ @api_version)
18
+
19
+ @session_id = response[:session_id]
20
+ @instance = response[:instance]
21
+ self
22
+ end
23
+
24
+ def org_id
25
+ @session_id.split("!").first
26
+ end
27
+
28
+ def create_job operation, sobject, content_type, external_field
29
+ Bulkforce::Http.create_job(
30
+ @instance,
31
+ @session_id,
32
+ operation,
33
+ sobject,
34
+ content_type,
35
+ @api_version,
36
+ external_field)[:id]
37
+ end
38
+
39
+ def close_job job_id
40
+ Bulkforce::Http.close_job(
41
+ @instance,
42
+ @session_id,
43
+ job_id,
44
+ @api_version)[:id]
45
+ end
46
+
47
+ def add_query job_id, data_or_soql
48
+ Bulkforce::Http.add_batch(
49
+ @instance,
50
+ @session_id,
51
+ job_id,
52
+ data_or_soql,
53
+ @api_version)[:id]
54
+ end
55
+
56
+ def query_batch job_id, batch_id
57
+ Bulkforce::Http.query_batch(
58
+ @instance,
59
+ @session_id,
60
+ job_id,
61
+ batch_id,
62
+ @api_version,
63
+ )
64
+ end
65
+
66
+ def query_batch_result_id job_id, batch_id
67
+ Bulkforce::Http.query_batch_result_id(
68
+ @instance,
69
+ @session_id,
70
+ job_id,
71
+ batch_id,
72
+ @api_version,
73
+ )
74
+ end
75
+
76
+ def query_batch_result_data job_id, batch_id, result_id
77
+ @raw_result = Bulkforce::Http.query_batch_result_data(
78
+ @instance,
79
+ @session_id,
80
+ job_id,
81
+ batch_id,
82
+ result_id,
83
+ @api_version,
84
+ )
85
+ Bulkforce::Helper.parse_csv @raw_result
86
+ end
87
+
88
+ def add_file_upload_batch job_id, filename
89
+ @raw_request = File.read(filename)
90
+ Bulkforce::Http.add_file_upload_batch(
91
+ @instance,
92
+ @session_id,
93
+ job_id,
94
+ @raw_request,
95
+ @api_version)[:id]
96
+ end
97
+
98
+ def add_batch job_id, records
99
+ return -1 if records.nil? || records.empty?
100
+ @raw_request = Bulkforce::Helper.records_to_csv(records)
101
+
102
+ Bulkforce::Http.add_batch(
103
+ @instance,
104
+ @session_id,
105
+ job_id,
106
+ @raw_request,
107
+ @api_version)[:id]
108
+ end
109
+
110
+ def self.connect(username, password, api_version, host)
111
+ self.new(username, password, api_version, host).login
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,40 @@
1
+ class Bulkforce
2
+ class ConnectionBuilder
3
+ attr_reader :host
4
+ attr_reader :api_version
5
+ attr_reader :credentials
6
+
7
+ def initialize(host:, api_version:, **credentials)
8
+ @host = host
9
+ @api_version = api_version
10
+ @credentials = credentials
11
+ end
12
+
13
+ def build
14
+ base_options = { api_version: api_version }
15
+
16
+ session_options = if credentials[:session_id]
17
+ {
18
+ session_id: credentials[:session_id],
19
+ instance: credentials.fetch(:instance),
20
+ }
21
+ elsif credentials[:refresh_token]
22
+ Bulkforce::Http.oauth_login(
23
+ host,
24
+ credentials.fetch(:client_id),
25
+ credentials.fetch(:client_secret),
26
+ credentials[:refresh_token]
27
+ )
28
+ else
29
+ Bulkforce::Http.login(
30
+ host,
31
+ credentials.fetch(:username),
32
+ "#{credentials.fetch(:password)}#{credentials.fetch(:security_token)}",
33
+ api_version
34
+ )
35
+ end.select { |k, _| [:session_id, :instance].include?(k) }.to_h
36
+
37
+ Bulkforce::Connection.new(base_options.merge(session_options))
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,77 @@
1
+ require "csv"
2
+
3
+ class Bulkforce
4
+ module Helper
5
+ extend self
6
+
7
+ CSV_OPTIONS = {
8
+ col_sep: ",",
9
+ quote_char: "\"",
10
+ force_quotes: true,
11
+ }
12
+
13
+ def records_to_csv records
14
+ file_mock = StringIO.new
15
+ csv_client = CSV.new(file_mock, CSV_OPTIONS)
16
+ all_headers = []
17
+ all_rows = []
18
+ records.each do |hash|
19
+ row = CSV::Row.new([],[],false)
20
+ to_store = hash.inject({}) do |h, (k, v)|
21
+ if v == nil || v == "" || v == []
22
+ h[k] = "#N/A"
23
+ else
24
+ h[k] = v.class == Array ? v.join(";") : v
25
+ end
26
+ h
27
+ end
28
+ row << to_store
29
+ all_headers << row.headers
30
+ all_rows << row
31
+ end
32
+ all_headers.flatten!.uniq!
33
+ csv_client << all_headers
34
+ all_rows.each do |row|
35
+ csv_client << row.fields(*all_headers)
36
+ end
37
+ file_mock.string
38
+ end
39
+
40
+ def fetch_instance_from_server_url server_url
41
+ before_sf = server_url[/^https?:\/\/(.+)\.salesforce\.com/, 1]
42
+ before_sf.gsub(/-api$/,"")
43
+ end
44
+
45
+ def attachment_keys records
46
+ records.map do |record|
47
+ record.select do |key, value|
48
+ value.class == File
49
+ end.keys
50
+ end.flatten.uniq
51
+ end
52
+
53
+ def transform_values! records, keys
54
+ keys.each do |key|
55
+ records.each do |record|
56
+ file_handle = record[key]
57
+ if file_handle
58
+ file_path = File.absolute_path(file_handle)
59
+ record
60
+ .merge!({
61
+ key => Bulkforce::Helper.absolute_to_relative_path(file_path,"#")
62
+ })
63
+ yield file_path if block_given?
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def absolute_to_relative_path input, replacement
70
+ input.gsub(/(^C:[\/\\])|(^\/)/,replacement)
71
+ end
72
+
73
+ def parse_csv csv_string
74
+ CSV.parse(csv_string, headers: true).map{|r| r.to_hash}
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,272 @@
1
+ require "net/https"
2
+ require "nori"
3
+ require "csv"
4
+
5
+ class Bulkforce
6
+ module Http
7
+ extend self
8
+
9
+ def login *args
10
+ r = Http::Request.login(*args)
11
+ process_soap_response(nori.parse(process_http_request(r)))
12
+ end
13
+
14
+ def oauth_login *args
15
+ r = Http::Request.oauth_login(*args)
16
+ process_oauth_response(nori.parse(process_http_request(r)))
17
+ end
18
+
19
+ def create_job *args
20
+ r = Http::Request.create_job(*args)
21
+ process_xml_response(nori.parse(process_http_request(r)))
22
+ end
23
+
24
+ def close_job *args
25
+ r = Http::Request.close_job(*args)
26
+ process_xml_response(nori.parse(process_http_request(r)))
27
+ end
28
+
29
+ def add_batch *args
30
+ r = Http::Request.add_batch(*args)
31
+ process_xml_response(nori.parse(process_http_request(r)))
32
+ end
33
+
34
+ def query_batch *args
35
+ r = Http::Request.query_batch(*args)
36
+ process_xml_response(nori.parse(process_http_request(r)))
37
+ end
38
+
39
+ def query_batch_result_id *args
40
+ r = Http::Request.query_batch_result_id(*args)
41
+ process_xml_response(nori.parse(process_http_request(r)))
42
+ end
43
+
44
+ def query_batch_result_data *args
45
+ r = Http::Request.query_batch_result_data(*args)
46
+ normalize_csv(process_http_request(r))
47
+ end
48
+
49
+ def add_file_upload_batch instance, session_id, job_id, data, api_version
50
+ headers = {
51
+ "Content-Type" => "zip/csv",
52
+ "X-SFDC-Session" => session_id}
53
+ r = Http::Request.new(
54
+ :post,
55
+ Http::Request.instance_host(instance),
56
+ "/services/async/#{api_version}/job/#{job_id}/batch",
57
+ data,
58
+ headers)
59
+ process_xml_response(nori.parse(process_http_request(r)))
60
+ end
61
+
62
+ def process_http_request(r)
63
+ http = Net::HTTP.new(r.host, 443)
64
+ http.use_ssl = true
65
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
66
+ http_request = Net::HTTP.
67
+ const_get(r.http_method.capitalize).
68
+ new(r.path, r.headers)
69
+ http_request.body = r.body if r.body
70
+ http.request(http_request).body
71
+ end
72
+
73
+ private
74
+ def nori
75
+ Nori.new(
76
+ :advanced_typecasting => true,
77
+ :strip_namespaces => true,
78
+ :convert_tags_to => lambda { |tag| tag.snakecase.to_sym })
79
+ end
80
+
81
+ def process_xml_response res
82
+ if res[:error]
83
+ raise "#{res[:error][:exception_code]}: #{res[:error][:exception_message]}"
84
+ end
85
+
86
+ res.values.first
87
+ end
88
+
89
+ def normalize_csv res
90
+ res.gsub(/\n\s+/, "\n")
91
+ end
92
+
93
+ def process_soap_response res
94
+ raw_result = res.fetch(:body){ res.fetch(:envelope).fetch(:body) }
95
+ raise raw_result[:fault][:faultstring] if raw_result[:fault]
96
+
97
+ login_result = raw_result[:login_response][:result]
98
+ instance = Helper.fetch_instance_from_server_url(login_result[:server_url])
99
+ login_result.merge(instance: instance)
100
+ end
101
+
102
+ def process_oauth_response res
103
+ inner = res.fetch(:o_auth)
104
+
105
+ if inner[:error]
106
+ raise "#{inner[:error]}: #{inner[:error_description]}"
107
+ end
108
+
109
+ {
110
+ server_url: inner.fetch(:instance_url),
111
+ session_id: inner.fetch(:access_token),
112
+ instance: Helper.fetch_instance_from_server_url(inner.fetch(:instance_url)),
113
+ }
114
+ end
115
+
116
+ class Request
117
+ attr_reader :path
118
+ attr_reader :host
119
+ attr_reader :body
120
+ attr_reader :headers
121
+ attr_reader :http_method
122
+
123
+ def initialize http_method, host, path, body, headers
124
+ @http_method = http_method
125
+ @host = host
126
+ @path = path
127
+ @body = body
128
+ @headers = headers
129
+ end
130
+
131
+ def self.login host, username, password, api_version
132
+ body = %Q{<?xml version="1.0" encoding="utf-8" ?>
133
+ <env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
134
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
135
+ xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
136
+ <env:Body>
137
+ <n1:login xmlns:n1="urn:partner.soap.sforce.com">
138
+ <n1:username>#{username}</n1:username>
139
+ <n1:password>#{password}</n1:password>
140
+ </n1:login>
141
+ </env:Body>
142
+ </env:Envelope>}
143
+ headers = {
144
+ "Content-Type" => "text/xml; charset=utf-8",
145
+ "SOAPAction" => "login"
146
+ }
147
+ Http::Request.new(
148
+ :post,
149
+ host,
150
+ "/services/Soap/u/#{api_version}",
151
+ body,
152
+ headers)
153
+ end
154
+
155
+ def self.oauth_login(host, client_id, client_secret, refresh_token)
156
+ headers = {
157
+ "Content-Type" => "application/x-www-form-urlencoded",
158
+ "Accept" => "application/xml",
159
+ }
160
+
161
+ body = {
162
+ grant_type: "refresh_token",
163
+ client_id: client_id,
164
+ client_secret: client_secret,
165
+ refresh_token: refresh_token
166
+ }.inject("") do |string, (k,v)|
167
+ string += "#{k}=#{v}&"
168
+ end
169
+
170
+ Http::Request.new(
171
+ :post,
172
+ host,
173
+ "/services/oauth2/token",
174
+ body,
175
+ headers)
176
+ end
177
+
178
+ def self.create_job instance, session_id, operation, sobject, content_type, api_version, external_field = nil
179
+ external_field_line = external_field ?
180
+ "<externalIdFieldName>#{external_field}</externalIdFieldName>" : nil
181
+ body = %Q{<?xml version="1.0" encoding="utf-8" ?>
182
+ <jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">
183
+ <operation>#{operation}</operation>
184
+ <object>#{sobject}</object>
185
+ #{external_field_line}
186
+ <contentType>#{content_type}</contentType>
187
+ </jobInfo>
188
+ }
189
+ headers = {
190
+ "Content-Type" => "application/xml; charset=utf-8",
191
+ "X-SFDC-Session" => session_id}
192
+ Http::Request.new(
193
+ :post,
194
+ instance_host(instance),
195
+ "/services/async/#{api_version}/job",
196
+ body,
197
+ headers)
198
+ end
199
+
200
+ def self.close_job instance, session_id, job_id, api_version
201
+ body = %Q{<?xml version="1.0" encoding="utf-8" ?>
202
+ <jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">
203
+ <state>Closed</state>
204
+ </jobInfo>
205
+ }
206
+ headers = {
207
+ "Content-Type" => "application/xml; charset=utf-8",
208
+ "X-SFDC-Session" => session_id}
209
+ Http::Request.new(
210
+ :post,
211
+ instance_host(instance),
212
+ "/services/async/#{api_version}/job/#{job_id}",
213
+ body,
214
+ headers)
215
+ end
216
+
217
+ def self.add_batch instance, session_id, job_id, data, api_version
218
+ headers = {"Content-Type" => "text/csv; charset=UTF-8", "X-SFDC-Session" => session_id}
219
+ Http::Request.new(
220
+ :post,
221
+ instance_host(instance),
222
+ "/services/async/#{api_version}/job/#{job_id}/batch",
223
+ data,
224
+ headers)
225
+ end
226
+
227
+ def self.query_batch instance, session_id, job_id, batch_id, api_version
228
+ headers = {"X-SFDC-Session" => session_id}
229
+ Http::Request.new(
230
+ :get,
231
+ instance_host(instance),
232
+ "/services/async/#{api_version}/job/#{job_id}/batch/#{batch_id}",
233
+ nil,
234
+ headers)
235
+ end
236
+
237
+ def self.query_batch_result_id instance, session_id, job_id, batch_id, api_version
238
+ headers = {
239
+ "Content-Type" => "application/xml; charset=utf-8",
240
+ "X-SFDC-Session" => session_id}
241
+ Http::Request.new(
242
+ :get,
243
+ instance_host(instance),
244
+ "/services/async/#{api_version}/job/#{job_id}/batch/#{batch_id}/result",
245
+ nil,
246
+ headers)
247
+ end
248
+
249
+ def self.query_batch_result_data(instance,
250
+ session_id,
251
+ job_id,
252
+ batch_id,
253
+ result_id,
254
+ api_version)
255
+ headers = {
256
+ "Content-Type" => "text/csv; charset=UTF-8",
257
+ "X-SFDC-Session" => session_id}
258
+ Http::Request.new(
259
+ :get,
260
+ instance_host(instance),
261
+ "/services/async/#{api_version}" \
262
+ "/job/#{job_id}/batch/#{batch_id}/result/#{result_id}",
263
+ nil,
264
+ headers)
265
+ end
266
+
267
+ def self.instance_host instance
268
+ "#{instance}.salesforce.com"
269
+ end
270
+ end
271
+ end
272
+ end