bulkforce 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.
@@ -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