bulkforce 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bd81802354ece9013e686bedab350cd6be171e7d
4
+ data.tar.gz: 57c3d0d6afe27d36e796b2cf017a47f0579a7623
5
+ SHA512:
6
+ metadata.gz: 65771ee34113f04a2d22c4bb6b1ede93356d0f66a84845f595b61cedafa0fb793a1702fb9bf55e3e866f66376dd54d84e762acb135fa4520ae53537fd65196e4
7
+ data.tar.gz: 375ea76a3395feada554bd313db8e80df6c4ee45377f81895454c573bc4f2574ae1258d35662336e28db17eb2125c919c2979af740bd47e13831776d27202b73
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ /.env
6
+ /vendor/bundle
7
+ /tmp
8
+ /coverage
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --tty --color --format documentation
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.2
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ cache: bundler
3
+ bundler_args: --without documentation production
4
+ rvm:
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1.5
8
+ - 2.2.2
9
+ - jruby-19mode
10
+ - rbx
11
+ - ruby-head
12
+ - jruby-head
13
+ matrix:
14
+ allow_failures:
15
+ - rvm: ruby-head
16
+ - rvm: jruby-head
17
+ fast_finish: true
18
+ script:
19
+ - bundle exec rake spec
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test, :development do
6
+ gem "coveralls", require: false
7
+ gem "guard"
8
+ gem "guard-rspec"
9
+ gem "dotenv"
10
+
11
+ gem "rb-inotify", require: false
12
+ gem "rb-fsevent", require: false
13
+ gem "rb-fchange", require: false
14
+ end
data/Guardfile ADDED
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: "rspec --tag ~type:integration" do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch("spec/spec_helper.rb") { "spec" }
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2012 Jorge Valdivia
3
+ Copyright (c) 2013 Leif Gensert, Propertybase GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # Bulkforce
2
+
3
+ [![Build Status](https://travis-ci.org/propertybase/bulkforce.png?branch=master)](https://travis-ci.org/propertybase/bulkforce) [![Coverage Status](https://coveralls.io/repos/propertybase/bulkforce/badge.png?branch=master)](https://coveralls.io/r/propertybase/bulkforce) [![Code Climate](https://codeclimate.com/github/propertybase/bulkforce.png)](https://codeclimate.com/github/propertybase/bulkforce) [![Dependency Status](https://gemnasium.com/propertybase/bulkforce.png)](https://gemnasium.com/propertybase/bulkforce) [![Gem Version](https://badge.fury.io/rb/bulkforce.png)](http://badge.fury.io/rb/bulkforce)
4
+
5
+ This is a fork of the originial [Executrix](http://github.com/propertybase/executrix) gem with some breaking API changes. In the long term, this gem will replace Executrix.
6
+
7
+ It is a Ruby MRI 2.1+ gem only.
8
+
9
+ ## Overview
10
+
11
+ Bulkforce is a simple ruby gem for connecting to and using the [Salesforce Bulk API](http://www.salesforce.com/us/developer/docs/api_asynch/index.htm). This gem only supports the functionality provided by the bulk API.
12
+
13
+ ## Installation
14
+
15
+ ~~~ sh
16
+ $ sudo gem install bulkforce
17
+ ~~~
18
+
19
+ ## How to use
20
+
21
+ After requiring using this gem is simple and straight forward.
22
+
23
+ ~~~ ruby
24
+ require "bulkforce"
25
+ ~~~
26
+
27
+ ### Authentication
28
+
29
+ The authentication is heavily inspired by [Restforce](https://github.com/ejholmes/restforce)
30
+
31
+ ### Session ID and instance
32
+
33
+ If you already have a session id and a instance you can directly authenticate against Salesforce:
34
+
35
+ ~~~ ruby
36
+ salesforce = Bulkforce.new(
37
+ session_id: "YOUR_SESSION_ID",
38
+ instance: "YOUR_INSTANCE",
39
+ )
40
+ ~~~
41
+
42
+ #### Username/Password/Security Token
43
+
44
+ Bulkforce supports basic authentication via username, password and security token.
45
+
46
+ ~~~ ruby
47
+ salesforce = Bulkforce.new(
48
+ username: "YOUR_SALESFORCE_USERNAME",
49
+ password: "YOUR_SALESFORCE_PASSWORD",
50
+ security_token: "YOUR_SALESFORCE_TOKEN,
51
+ )
52
+ ~~~
53
+
54
+ #### OAuth
55
+
56
+ You can also authenticate via OAuth. Therefore you need the `client_id`, `client_secret` and `refresh_token`.
57
+
58
+ ~~~ ruby
59
+ salesforce = Bulkforce.new(
60
+ client_id: "YOUR_CLIENT_ID",
61
+ client_secret: "YOUR_CLIENT_SECRET",
62
+ refresh_token: "YOUR_REFRESH_TOKEN,
63
+ )
64
+ ~~~
65
+
66
+ #### Priority
67
+
68
+ If you define credentials for multiple authentication methods, the following priority applies:
69
+
70
+ - Session ID
71
+ - OAuth
72
+ - User/Password/Security Token
73
+
74
+ #### ENV variables
75
+
76
+ You can also define the credentials in Environment variables which are as following:
77
+
78
+ ~~~
79
+ SALESFORCE_API_VERSION=...
80
+ SALESFORCE_USERNAME=...
81
+ SALESFORCE_PASSWORD=...
82
+ SALESFORCE_SECURITY_TOKEN=...
83
+ SALESFORCE_HOST=...
84
+ SALESFORCE_CLIENT_ID=...
85
+ SALESFORCE_CLIENT_SECRET=...
86
+ SALESFORCE_INSTANCE=...
87
+ SALESFORCE_REFRESH_TOKEN=...
88
+ ~~~
89
+
90
+ Afterwards you can just instantiate the client:
91
+
92
+ ~~~ ruby
93
+ Bulkforce.new
94
+ ~~~
95
+
96
+ ### Sandbox
97
+
98
+ To use the bulkforce against your salesforce sandbox, change the host to `test.salesforce.com`
99
+
100
+ ~~~ ruby
101
+ salesforce = Bulkforce.new(
102
+ # credentials
103
+ host: "test.salesforce.com",
104
+ )
105
+ ~~~
106
+
107
+ ### OrgId
108
+
109
+ After you created the client object you can fetch the OrgId via `org_id`.
110
+
111
+ This will fetch the 15 digit OrgId.
112
+
113
+ ~~~ ruby
114
+ salesforce.org_id # "00D50000000IehZ"
115
+ ~~~
116
+
117
+ ### Operations
118
+
119
+ ~~~ ruby
120
+ # Insert
121
+ new_account = {"name" => "Test Account", "type" => "Other"} # Add as many fields per record as needed.
122
+ records_to_insert = []
123
+ records_to_insert << new_account # You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
124
+ result = salesforce.insert("Account", records_to_insert)
125
+ puts "reference to the bulk job: #{result.inspect}"
126
+ ~~~
127
+
128
+ ~~~ ruby
129
+ # Update
130
+ updated_account = {"name" => "Test Account -- Updated", "id" => "a00A0001009zA2m"} # Nearly identical to an insert, but we need to pass the salesforce id.
131
+ records_to_update = []
132
+ records_to_update.push(updated_account)
133
+ salesforce.update("Account", records_to_update)
134
+ ~~~
135
+
136
+ ~~~ ruby
137
+ # Upsert
138
+ upserted_account = {"name" => "Test Account -- Upserted", "External_Field_Name" => "123456"} # Fields to be updated. External field must be included
139
+ records_to_upsert = []
140
+ records_to_upsert.push(upserted_account)
141
+ salesforce.upsert("Account", records_to_upsert, "External_Field_Name") # Note that upsert accepts an extra parameter for the external field name
142
+ ~~~
143
+
144
+ ~~~ ruby
145
+ # Delete
146
+ deleted_account = {"id" => "a00A0001009zA2m"} # We only specify the id of the records to delete
147
+ records_to_delete = []
148
+ records_to_delete.push(deleted_account)
149
+ salesforce.delete("Account", records_to_delete)
150
+ ~~~
151
+
152
+ ~~~ ruby
153
+ # Query
154
+ res = salesforce.query("Account", "select id, name, createddate from Account limit 3") # We just need to pass the sobject name and the query string
155
+ puts res.result.records.inspect
156
+ ~~~
157
+
158
+ ## File Upload
159
+
160
+ For file uploads, just add a `File` object to the binary columns.
161
+ ~~~ ruby
162
+ attachment = {"ParentId" => "00Kk0001908kqkDEAQ", "Name" => "attachment.pdf", "Body" => File.new("tmp/attachment.pdf")}
163
+ records_to_insert = []
164
+ records_to_insert << attachment
165
+ salesforce.insert("Attachment", records_to_insert)
166
+ ~~~
167
+
168
+ ### Query status
169
+
170
+ The above examples all return immediately after sending the data to the Bulk API. If you want to wait, until the batch finished, call the final_status method on the batch-reference.
171
+
172
+ ~~~ ruby
173
+ new_account = {"name" => "Test Account", "type" => "Other"} # Add as many fields per record as needed.
174
+ records_to_insert = []
175
+ records_to_insert << new_account # You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
176
+ batch_reference = salesforce.insert("Account", records_to_insert)
177
+ results = batch_reference.final_status
178
+ puts "the results: #{results.inspect}"
179
+ ~~~
180
+
181
+ Additionally you cann pass in a block to query the current state of the batch job:
182
+
183
+ ~~~ ruby
184
+ new_account = {"name" => "Test Account", "type" => "Other"} # Add as many fields per record as needed.
185
+ records_to_insert = []
186
+ records_to_insert << new_account # You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
187
+ batch_reference = salesforce.insert("Account", records_to_insert)
188
+ results = batch_reference.final_status do |status|
189
+ puts "running: #{status.inspect}"
190
+ end
191
+ puts "the results: #{results.inspect}"
192
+ ~~~
193
+
194
+ The block will yield every 2 seconds, but you can also specify the poll interval:
195
+
196
+ ~~~ ruby
197
+ new_account = {"name" => "Test Account", "type" => "Other"} # Add as many fields per record as needed.
198
+ records_to_insert = []
199
+ records_to_insert << new_account # You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
200
+ batch_reference = salesforce.insert("Account", records_to_insert)
201
+ poll_interval = 10
202
+ results = batch_reference.final_status(poll_interval) do |status|
203
+ puts "running: #{status.inspect}"
204
+ end
205
+ puts "the results: #{results.inspect}"
206
+ ~~~
207
+
208
+ ## Copyright
209
+
210
+ Copyright (c) 2012 Jorge Valdivia.
211
+ Copyright (c) 2015 Leif Gensert, [Propertybase GmbH](http://propertybase.com)
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
data/bulkforce.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "bulkforce/version"
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "bulkforce"
8
+ gem.version = Bulkforce::VERSION
9
+ gem.authors = ["Jorge Valdivia", "Leif Gensert"]
10
+ gem.email = ["jorge@valdivia.me", "leif@propertybase.com"]
11
+ gem.homepage = "https://github.com/propertybase/bulkforce"
12
+ gem.summary = %q{Ruby support for the Salesforce Bulk API}
13
+ gem.description = %q{This gem provides a super simple interface for the Salesforce Bulk API. It provides support for insert, update, upsert, delete, and query.}
14
+ gem.license = "MIT"
15
+
16
+ gem.rubyforge_project = "bulkforce"
17
+
18
+ gem.files = `git ls-files`.split("\n")
19
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ gem.require_paths = ["lib"]
22
+
23
+ gem.add_dependency "rake"
24
+ gem.add_dependency "json"
25
+ gem.add_dependency "nori", "< 2.7"
26
+ gem.add_dependency "nokogiri", "< 1.7"
27
+ gem.add_dependency "rubyzip", ">= 1.0" ,"< 1.2"
28
+ gem.add_development_dependency "rspec", "< 3.4"
29
+ gem.add_development_dependency "webmock", "< 1.22"
30
+ if RUBY_ENGINE == "rbx"
31
+ gem.add_dependency "rubysl", "< 2.2"
32
+ gem.add_development_dependency "racc", "< 1.5"
33
+ gem.add_development_dependency "rubinius-coverage", "< 2.1"
34
+ end
35
+ end
data/lib/bulkforce.rb ADDED
@@ -0,0 +1,109 @@
1
+ require "bulkforce/version"
2
+ require "bulkforce/configuration"
3
+ require "bulkforce/helper"
4
+ require "bulkforce/batch"
5
+ require "bulkforce/http"
6
+ require "bulkforce/connection_builder"
7
+ require "bulkforce/connection"
8
+ require "zip"
9
+
10
+ class Bulkforce
11
+ SALESFORCE_API_VERSION = "33.0"
12
+
13
+ class << self
14
+ attr_accessor :configuration
15
+ end
16
+
17
+ def initialize(options = {})
18
+ configuration = self.class.configuration || Configuration.new
19
+ merged_opts = configuration.to_h.merge(options)
20
+
21
+ unless merged_opts[:host] =~ /salesforce.com\/?$/
22
+ warn("WARNING: You are submitting credentials to a host other than salesforce.com")
23
+ end
24
+
25
+ @connection = Bulkforce::ConnectionBuilder.new(merged_opts).build
26
+ end
27
+
28
+ def org_id
29
+ @connection.org_id
30
+ end
31
+
32
+ def upsert(sobject, records, external_field)
33
+ start_job("upsert", sobject, records, external_field)
34
+ end
35
+
36
+ def update(sobject, records)
37
+ start_job("update", sobject, records)
38
+ end
39
+
40
+ def insert(sobject, records)
41
+ start_job("insert", sobject, records)
42
+ end
43
+
44
+ def delete(sobject, records)
45
+ start_job("delete", sobject, records)
46
+ end
47
+
48
+ def query(sobject, query)
49
+ job_id = @connection.create_job(
50
+ "query",
51
+ sobject,
52
+ "CSV",
53
+ nil)
54
+ batch_id = @connection.add_query(job_id, query)
55
+ @connection.close_job job_id
56
+ batch_reference = Bulkforce::Batch.new @connection, job_id, batch_id
57
+ batch_reference.final_status
58
+ end
59
+
60
+ def self.configure(&block)
61
+ self.configuration = Configuration.new
62
+ block.call(self.configuration)
63
+ end
64
+
65
+ private
66
+ def start_job(operation, sobject, records, external_field=nil)
67
+ attachment_keys = Bulkforce::Helper.attachment_keys(records)
68
+
69
+ content_type = "CSV"
70
+ zip_filename = nil
71
+ request_filename = nil
72
+ batch_id = -1
73
+ if not attachment_keys.empty?
74
+ tmp_filename = Dir::Tmpname.make_tmpname("bulk_upload", ".zip")
75
+ zip_filename = "#{Dir.tmpdir}/#{tmp_filename}"
76
+ Zip::File.open(zip_filename, Zip::File::CREATE) do |zipfile|
77
+ Bulkforce::Helper.transform_values!(records, attachment_keys) do |path|
78
+ relative_path = Bulkforce::Helper.absolute_to_relative_path(path, "")
79
+ zipfile.add(relative_path, path) rescue Zip::ZipEntryExistsError
80
+ end
81
+ tmp_filename = Dir::Tmpname.make_tmpname("request", ".txt")
82
+ request_filename = "#{Dir.tmpdir}/#{tmp_filename}"
83
+ File.open(request_filename, "w") do |file|
84
+ file.write(Bulkforce::Helper.records_to_csv(records))
85
+ end
86
+ zipfile.add("request.txt", request_filename)
87
+ end
88
+
89
+ content_type = "ZIP_CSV"
90
+ end
91
+
92
+ job_id = @connection.create_job(
93
+ operation,
94
+ sobject,
95
+ content_type,
96
+ external_field)
97
+ if zip_filename
98
+ batch_id = @connection.add_file_upload_batch job_id, zip_filename
99
+ [zip_filename, request_filename].each do |file|
100
+ File.delete(file) if file
101
+ end
102
+ else
103
+ batch_id = @connection.add_batch job_id, records
104
+ end
105
+
106
+ @connection.close_job job_id
107
+ Bulkforce::Batch.new @connection, job_id, batch_id
108
+ end
109
+ end