├── .gitignore
├── .rspec
├── Gemfile
├── LICENCE
├── README.md
├── Rakefile
├── auth_credentials.yml.example
├── lib
├── salesforce_bulk_api.rb
└── salesforce_bulk_api
│ ├── concerns
│ └── throttling.rb
│ ├── connection.rb
│ ├── job.rb
│ └── version.rb
├── salesforce_bulk_api.gemspec
└── spec
├── salesforce_bulk_api
└── salesforce_bulk_api_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | .ruby-gemset
5 | .ruby-version
6 | pkg/*
7 | auth_credentials.yml
8 | *.swp
9 | .idea
10 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format progress
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in salesforce_bulk_api.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2025 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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Salesforce-Bulk-Api
2 |
3 | [](http://badge.fury.io/rb/salesforce_bulk_api)
4 |
5 | ## Table of Contents
6 |
7 | - [Overview](#overview)
8 | - [Installation](#installation)
9 | - [Authentication](#authentication)
10 | - [Usage](#usage)
11 | - [Basic Operations](#basic-operations)
12 | - [Job Management](#job-management)
13 | - [Event Listening](#event-listening)
14 | - [Retrieving Batch Records](#retrieving-batch-records)
15 | - [API Call Throttling](#api-call-throttling)
16 | - [Contributing](#contributing)
17 | - [License](#license)
18 |
19 | ## Overview
20 |
21 | `SalesforceBulkApi` is a Ruby wrapper for the Salesforce Bulk API. It is rewritten from [salesforce_bulk](https://github.com/jorgevaldivia/salesforce_bulk) and adds several missing features, making it easier to perform bulk operations with Salesforce from Ruby applications.
22 |
23 | ## Installation
24 |
25 | Add this line to your application's Gemfile:
26 |
27 | ```ruby
28 | gem 'salesforce_bulk_api'
29 | ```
30 |
31 | And then execute:
32 |
33 | ```
34 | bundle install
35 | ```
36 |
37 | Or install it directly:
38 |
39 | ```
40 | gem install salesforce_bulk_api
41 | ```
42 |
43 | ## Authentication
44 |
45 | You can authenticate with Salesforce using either `databasedotcom` or `restforce` gems. Both support various authentication methods including username/password, OmniAuth, and OAuth2.
46 |
47 | Please refer to the documentation of these gems for detailed authentication options:
48 |
49 | - [Databasedotcom](https://github.com/heroku/databasedotcom)
50 | - [Restforce](https://github.com/ejholmes/restforce)
51 |
52 | ### Authentication Examples
53 |
54 | #### Using Databasedotcom:
55 |
56 | ```ruby
57 | require 'salesforce_bulk_api'
58 |
59 | client = Databasedotcom::Client.new(
60 | client_id: SFDC_APP_CONFIG["client_id"],
61 | client_secret: SFDC_APP_CONFIG["client_secret"]
62 | )
63 | client.authenticate(
64 | token: " ",
65 | instance_url: "http://na1.salesforce.com"
66 | )
67 |
68 | salesforce = SalesforceBulkApi::Api.new(client)
69 | ```
70 |
71 | #### Using Restforce:
72 |
73 | ```ruby
74 | require 'salesforce_bulk_api'
75 |
76 | client = Restforce.new(
77 | username: SFDC_APP_CONFIG['SFDC_USERNAME'],
78 | password: SFDC_APP_CONFIG['SFDC_PASSWORD'],
79 | security_token: SFDC_APP_CONFIG['SFDC_SECURITY_TOKEN'],
80 | client_id: SFDC_APP_CONFIG['SFDC_CLIENT_ID'],
81 | client_secret: SFDC_APP_CONFIG['SFDC_CLIENT_SECRET'],
82 | host: SFDC_APP_CONFIG['SFDC_HOST']
83 | )
84 | client.authenticate!
85 |
86 | salesforce = SalesforceBulkApi::Api.new(client)
87 | ```
88 |
89 | ## Usage
90 |
91 | ### Basic Operations
92 |
93 | #### Create/Insert Records
94 |
95 | ```ruby
96 | new_account = { "name" => "Test Account", "type" => "Other" }
97 | records_to_insert = [new_account]
98 | result = salesforce.create("Account", records_to_insert)
99 | puts "Result: #{result.inspect}"
100 | ```
101 |
102 | #### Update Records
103 |
104 | ```ruby
105 | updated_account = { "name" => "Test Account -- Updated", "id" => "a00A0001009zA2m" }
106 | records_to_update = [updated_account]
107 | salesforce.update("Account", records_to_update)
108 | ```
109 |
110 | #### Upsert Records
111 |
112 | ```ruby
113 | upserted_account = { "name" => "Test Account -- Upserted", "External_Field_Name" => "123456" }
114 | records_to_upsert = [upserted_account]
115 | salesforce.upsert("Account", records_to_upsert, "External_Field_Name")
116 | ```
117 |
118 | #### Delete Records
119 |
120 | ```ruby
121 | deleted_account = { "id" => "a00A0001009zA2m" }
122 | records_to_delete = [deleted_account]
123 | salesforce.delete("Account", records_to_delete)
124 | ```
125 |
126 | #### Query Records
127 |
128 | ```ruby
129 | res = salesforce.query("Account", "SELECT id, name, createddate FROM Account LIMIT 3")
130 | ```
131 |
132 | ### Job Management
133 |
134 | You can check the status of a job using its ID:
135 |
136 | ```ruby
137 | job = salesforce.job_from_id('a00A0001009zA2m')
138 | puts "Status: #{job.check_job_status.inspect}"
139 | ```
140 |
141 | ### Event Listening
142 |
143 | You can listen for job creation events:
144 |
145 | ```ruby
146 | salesforce.on_job_created do |job|
147 | puts "Job #{job.job_id} created!"
148 | end
149 | ```
150 |
151 | ### Retrieving Batch Records
152 |
153 | Fetch records from a specific batch in a job:
154 |
155 | ```ruby
156 | job_id = 'l02A0231009Za8m'
157 | batch_id = 'H24a0708089zA2J'
158 | records = salesforce.get_batch_records(job_id, batch_id)
159 | ```
160 |
161 | ### API Call Throttling
162 |
163 | You can control how frequently status checks are performed:
164 |
165 | ```ruby
166 | # Set status check interval to 30 seconds
167 | salesforce.connection.set_status_throttle(30)
168 | ```
169 |
170 | ## Contributing
171 |
172 | We welcome contributions to improve this gem. Feel free to:
173 |
174 | 1. Fork the repository
175 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
176 | 3. Commit your changes (`git commit -am 'Add some amazing feature'`)
177 | 4. Push to the branch (`git push origin feature/amazing-feature`)
178 | 5. Create a new Pull Request
179 |
180 | ## License
181 |
182 | This project is licensed under the MIT License, Copyright (c) 2025 - see the [LICENCE](LICENCE) file for details.
183 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "rspec/core/rake_task"
2 | task default: :spec
3 | RSpec::Core::RakeTask.new
4 |
--------------------------------------------------------------------------------
/auth_credentials.yml.example:
--------------------------------------------------------------------------------
1 | salesforce:
2 | client_id: client_id_here
3 | client_secret: client_secret_here
4 | user: sf_user@example.com
5 | passwordandtoken: passandtokenhere
6 | test_account_id: 0013000000ymMBh
7 | host: 'login.salesforce.com' # use test.salesforce.com if it is a sandbox
8 |
--------------------------------------------------------------------------------
/lib/salesforce_bulk_api.rb:
--------------------------------------------------------------------------------
1 | require "rubygems"
2 | require "bundler"
3 | require "net/https"
4 | require "xmlsimple"
5 | require "csv"
6 |
7 | require "salesforce_bulk_api/version"
8 | require "salesforce_bulk_api/concerns/throttling"
9 | require "salesforce_bulk_api/job"
10 | require "salesforce_bulk_api/connection"
11 |
12 | module SalesforceBulkApi
13 | class Api
14 | attr_reader :connection
15 |
16 | def initialize(client, salesforce_api_version = "46.0")
17 | @connection = SalesforceBulkApi::Connection.new(salesforce_api_version, client)
18 | @listeners = {job_created: []}
19 | @counters = Hash.new(0)
20 | end
21 |
22 | %w[upsert update create delete].each do |operation|
23 | define_method(operation) do |sobject, records, external_field = nil, **options|
24 | do_operation(operation, sobject, records, external_field, **options)
25 | end
26 | end
27 |
28 | def query(sobject, query, **)
29 | do_operation("query", sobject, query, nil, get_response: true, **)
30 | end
31 |
32 | def counters
33 | {
34 | http_get: @connection.counters[:get],
35 | http_post: @connection.counters[:post],
36 | upsert: @counters[:upsert],
37 | update: @counters[:update],
38 | create: @counters[:create],
39 | delete: @counters[:delete],
40 | query: @counters[:query]
41 | }
42 | end
43 |
44 | # Allows you to attach a listener that accepts the created job (which has a useful #job_id field).
45 | # This is useful for recording a job ID persistently before you begin batch work (i.e. start modifying the salesforce database),
46 | # so if the load process you are writing needs to recover, it can be aware of previous jobs it started and wait
47 | # for them to finish.
48 | #
49 | def on_job_created(&block)
50 | @listeners[:job_created] << block
51 | end
52 |
53 | def job_from_id(job_id)
54 | SalesforceBulkApi::Job.new(job_id: job_id, connection: @connection)
55 | end
56 |
57 | private
58 |
59 | def do_operation(operation, sobject, records, external_field, **options)
60 | count(operation.to_sym)
61 |
62 | job = SalesforceBulkApi::Job.new(
63 | operation: operation,
64 | sobject: sobject,
65 | records: records,
66 | external_field: external_field,
67 | connection: @connection
68 | )
69 |
70 | job.create_job(options[:batch_size], options[:send_nulls], options[:no_null_list])
71 | @listeners[:job_created].each { |callback| callback.call(job) }
72 |
73 | (operation == "query") ? job.add_query : job.add_batches
74 |
75 | response = job.close_job
76 | response.merge!("batches" => job.get_job_result(options[:get_response], options[:timeout])) if options[:get_response]
77 | response
78 | end
79 |
80 | def count(name)
81 | @counters[name] += 1
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/salesforce_bulk_api/concerns/throttling.rb:
--------------------------------------------------------------------------------
1 | module SalesforceBulkApi::Concerns
2 | module Throttling
3 | def throttles
4 | @throttles.dup
5 | end
6 |
7 | def add_throttle(&throttling_callback)
8 | @throttles ||= []
9 | @throttles << throttling_callback
10 | end
11 |
12 | def set_status_throttle(limit_seconds)
13 | set_throttle_limit_in_seconds(limit_seconds, [:http_method, :path], ->(details) { details[:http_method] == :get })
14 | end
15 |
16 | def set_throttle_limit_in_seconds(limit_seconds, throttle_by_keys, only_if)
17 | add_throttle do |details|
18 | limit_log = get_limit_log(Time.now - limit_seconds)
19 | key = extract_constraint_key_from(details, throttle_by_keys)
20 | last_request = limit_log[key]
21 |
22 | if last_request && only_if.call(details)
23 | seconds_since_last_request = Time.now.to_f - last_request.to_f
24 | need_to_wait_seconds = limit_seconds - seconds_since_last_request
25 | sleep(need_to_wait_seconds) if need_to_wait_seconds.positive?
26 | end
27 |
28 | limit_log[key] = Time.now
29 | end
30 | end
31 |
32 | private
33 |
34 | def extract_constraint_key_from(details, throttle_by_keys)
35 | throttle_by_keys.each_with_object({}) { |k, hash| hash[k] = details[k] }
36 | end
37 |
38 | def get_limit_log(prune_older_than)
39 | @limits ||= {}
40 | @limits.delete_if { |_, v| v < prune_older_than }
41 | end
42 |
43 | def throttle(details = {})
44 | (@throttles || []).each do |callback|
45 | callback.call(details)
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/salesforce_bulk_api/connection.rb:
--------------------------------------------------------------------------------
1 | require "timeout"
2 | require "net/https"
3 |
4 | module SalesforceBulkApi
5 | class Connection
6 | include Concerns::Throttling
7 |
8 | LOGIN_HOST = "login.salesforce.com".freeze
9 |
10 | attr_reader :session_id, :server_url, :instance, :instance_host
11 |
12 | def initialize(api_version, client)
13 | @client = client
14 | @api_version = api_version
15 | @path_prefix = "/services/async/#{@api_version}/"
16 | @counters = Hash.new(0)
17 |
18 | login
19 | end
20 |
21 | def post_xml(host, path, xml, headers)
22 | host ||= @instance_host
23 | headers["X-SFDC-Session"] = @session_id unless host == LOGIN_HOST
24 | path = "#{@path_prefix}#{path}" unless host == LOGIN_HOST
25 |
26 | perform_request(:post, host, path, xml, headers)
27 | end
28 |
29 | def get_request(host, path, headers)
30 | host ||= @instance_host
31 | path = "#{@path_prefix}#{path}"
32 | headers["X-SFDC-Session"] = @session_id unless host == LOGIN_HOST
33 |
34 | perform_request(:get, host, path, nil, headers)
35 | end
36 |
37 | def counters
38 | {
39 | get: @counters[:get],
40 | post: @counters[:post]
41 | }
42 | end
43 |
44 | private
45 |
46 | def login
47 | client_type = @client.class.to_s
48 | @session_id, @server_url = if client_type == "Restforce::Data::Client"
49 | [@client.options[:oauth_token], @client.options[:instance_url]]
50 | else
51 | [@client.oauth_token, @client.instance_url]
52 | end
53 | @instance = parse_instance
54 | @instance_host = "#{@instance}.salesforce.com"
55 | end
56 |
57 | def perform_request(method, host, path, body, headers)
58 | retries = 0
59 | begin
60 | count(method)
61 | throttle(http_method: method, path: path)
62 | response = https(host).public_send(method, path, body, headers)
63 | response.body
64 | rescue => e
65 | retries += 1
66 | if retries < 3
67 | puts "Request fail #{retries}: Retrying #{path}"
68 | retry
69 | else
70 | puts "FATAL: Request to #{path} failed three times."
71 | raise e
72 | end
73 | end
74 | end
75 |
76 | def https(host)
77 | Net::HTTP.new(host, 443).tap do |http|
78 | http.use_ssl = true
79 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
80 | end
81 | end
82 |
83 | def count(http_method)
84 | @counters[http_method] += 1
85 | end
86 |
87 | def parse_instance
88 | instance = @server_url.match(%r{https://([a-z]{2}[0-9]{1,2})\.})&.captures&.first
89 | instance || @server_url.split(".salesforce.com").first.split("://").last
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/salesforce_bulk_api/job.rb:
--------------------------------------------------------------------------------
1 | require "timeout"
2 |
3 | module SalesforceBulkApi
4 | class Job
5 | attr_reader :job_id
6 |
7 | class SalesforceException < StandardError; end
8 |
9 | XML_HEADER = ''.freeze
10 |
11 | def initialize(args)
12 | @job_id = args[:job_id]
13 | @operation = args[:operation]
14 | @sobject = args[:sobject]
15 | @external_field = args[:external_field]
16 | @records = args[:records]
17 | @connection = args[:connection]
18 | @batch_ids = []
19 | end
20 |
21 | def create_job(batch_size, send_nulls, no_null_list)
22 | @batch_size = batch_size
23 | @send_nulls = send_nulls
24 | @no_null_list = no_null_list
25 |
26 | xml = build_job_xml
27 | response = post_xml("job", xml)
28 | parse_job_response(response)
29 | end
30 |
31 | def close_job
32 | xml = build_close_job_xml
33 | response = post_xml("job/#{@job_id}", xml)
34 | XmlSimple.xml_in(response)
35 | end
36 |
37 | def add_query
38 | response = post_xml("job/#{@job_id}/batch/", @records)
39 | response_parsed = XmlSimple.xml_in(response)
40 | @batch_ids << response_parsed["id"][0]
41 | end
42 |
43 | def add_batches
44 | raise ArgumentError, "Records must be an array of hashes." unless @records.is_a?(Array)
45 |
46 | keys = @records.each_with_object({}) { |pairs, h| pairs.each { |k, v| (h[k] ||= []) << v } }.keys
47 | batches = @records.each_slice(@batch_size).to_a
48 |
49 | batches.each do |batch|
50 | @batch_ids << add_batch(keys, batch)
51 | end
52 | end
53 |
54 | def get_job_result(return_result, timeout)
55 | state = []
56 | Timeout.timeout(timeout, JobTimeout) do
57 | loop do
58 | job_status = check_job_status
59 | break unless job_closed_and_batches_completed?(job_status, state)
60 | break if @batch_ids.empty?
61 | end
62 | end
63 | rescue JobTimeout => e
64 | handle_timeout(e)
65 | ensure
66 | process_batch_results(state) if return_result
67 | state
68 | end
69 |
70 | private
71 |
72 | def build_job_xml
73 | xml = "#{XML_HEADER}"
74 | xml << "#{@operation}"
75 | xml << ""
76 | xml << "#{@external_field}" if @external_field
77 | xml << "XML"
78 | xml << ""
79 | end
80 |
81 | def build_close_job_xml
82 | "#{XML_HEADER}Closed"
83 | end
84 |
85 | def post_xml(path, xml)
86 | headers = {"Content-Type" => "application/xml; charset=utf-8"}
87 | @connection.post_xml(nil, path, xml, headers)
88 | end
89 |
90 | def parse_job_response(response)
91 | response_parsed = XmlSimple.xml_in(response)
92 | if response_parsed["exceptionCode"]
93 | raise SalesforceException, "#{response_parsed["exceptionMessage"][0]} (#{response_parsed["exceptionCode"][0]})"
94 | end
95 | @job_id = response_parsed["id"][0]
96 | end
97 |
98 | def add_batch(keys, batch)
99 | xml = "#{XML_HEADER}"
100 | batch.each { |r| xml << create_sobject(keys, r) }
101 | xml << ""
102 |
103 | response = post_xml("job/#{@job_id}/batch/", xml)
104 | response_parsed = XmlSimple.xml_in(response)
105 | response_parsed["id"]&.first
106 | end
107 |
108 | def job_closed_and_batches_completed?(job_status, state)
109 | return false unless job_status && job_status["state"] && job_status["state"][0] == "Closed"
110 |
111 | batch_statuses = {}
112 | batches_ready = @batch_ids.all? do |batch_id|
113 | batch_state = batch_statuses[batch_id] = check_batch_status(batch_id)
114 | batch_state && batch_state["state"] && batch_state["state"][0] && !["Queued", "InProgress"].include?(batch_state["state"][0])
115 | end
116 |
117 | if batches_ready
118 | @batch_ids.each do |batch_id|
119 | state.unshift(batch_statuses[batch_id])
120 | @batch_ids.delete(batch_id)
121 | end
122 | end
123 |
124 | true
125 | end
126 |
127 | def handle_timeout(error)
128 | puts "Timeout waiting for Salesforce to process job batches #{@batch_ids} of job #{@job_id}."
129 | puts error
130 | raise
131 | end
132 |
133 | def process_batch_results(state)
134 | state.each_with_index do |batch_state, i|
135 | if batch_state["state"][0] == "Completed"
136 | state[i].merge!("response" => get_batch_result(batch_state["id"][0]))
137 | end
138 | end
139 | end
140 | end
141 |
142 | class JobTimeout < StandardError; end
143 | end
144 |
--------------------------------------------------------------------------------
/lib/salesforce_bulk_api/version.rb:
--------------------------------------------------------------------------------
1 | module SalesforceBulkApi
2 | VERSION = "1.2.0"
3 | end
4 |
--------------------------------------------------------------------------------
/salesforce_bulk_api.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 | require "salesforce_bulk_api/version"
3 |
4 | Gem::Specification.new do |s|
5 | s.name = "salesforce_bulk_api"
6 | s.version = SalesforceBulkApi::VERSION
7 | s.authors = ["Yatish Mehta"]
8 | s.email = ["yatish27@users.noreply.github.com"]
9 |
10 | s.homepage = "https://github.com/yatishmehta27/salesforce_bulk_api"
11 | s.summary = "It uses the bulk api of salesforce to communicate with Salesforce CRM"
12 | s.description = "Salesforce Bulk API with governor limits taken care of"
13 |
14 | s.add_dependency("json", [">= 0"])
15 | s.add_dependency("xml-simple", [">= 0"])
16 | s.add_dependency("csv", [">= 0"])
17 |
18 | s.add_development_dependency "rspec"
19 | s.add_development_dependency "restforce", "~> 3.0.0"
20 | s.add_development_dependency "rake", ">= 12.3.3"
21 | s.add_development_dependency "pry"
22 | s.add_development_dependency "standardrb"
23 |
24 | s.files = `git ls-files`.split("\n")
25 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
26 | s.require_paths = ["lib"]
27 | end
28 |
--------------------------------------------------------------------------------
/spec/salesforce_bulk_api/salesforce_bulk_api_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "yaml"
3 | require "restforce"
4 |
5 | describe SalesforceBulkApi do
6 | before :each do
7 | auth_hash = YAML.load_file("auth_credentials.yml")
8 | sfdc_auth_hash = auth_hash["salesforce"]
9 |
10 | @sf_client = Restforce.new(
11 | username: sfdc_auth_hash["user"],
12 | password: sfdc_auth_hash["passwordandtoken"],
13 | client_id: sfdc_auth_hash["client_id"],
14 | client_secret: sfdc_auth_hash["client_secret"],
15 | host: sfdc_auth_hash["host"]
16 | )
17 | @sf_client.authenticate!
18 |
19 | @account_id = auth_hash["salesforce"]["test_account_id"]
20 |
21 | @api = SalesforceBulkApi::Api.new(@sf_client)
22 | end
23 |
24 | after :each do
25 | end
26 |
27 | describe "upsert" do
28 | context "when not passed get_result" do
29 | it "doesn't return the batches array" do
30 | res = @api.upsert("Account", [{Id: @account_id, Website: "www.test.com"}], "Id")
31 | res["batches"].should be_nil
32 | end
33 | end
34 |
35 | context "when passed get_result = true" do
36 | it "returns the batches array" do
37 | res = @api.upsert("Account", [{Id: @account_id, Website: "www.test.com"}], "Id", true)
38 | res["batches"][0]["response"].is_a? Array
39 |
40 | res["batches"][0]["response"][0]["id"][0].should start_with(@account_id)
41 | res["batches"][0]["response"][0]["success"].should eq ["true"]
42 | res["batches"][0]["response"][0]["created"].should eq ["false"]
43 | end
44 | end
45 |
46 | context "when passed send_nulls = true" do
47 | it "sets the nil and empty attributes to NULL" do
48 | @api.update("Account", [{Id: @account_id, Website: "abc123", Phone: "5678"}], true)
49 | res = @api.query("Account", "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
50 | res["batches"][0]["response"][0]["Website"][0].should eq "abc123"
51 | res["batches"][0]["response"][0]["Phone"][0].should eq "5678"
52 | res = @api.upsert("Account", [{Id: @account_id, Website: "", Phone: nil}], "Id", true, true)
53 | res["batches"][0]["response"][0]["id"][0].should start_with(@account_id)
54 | res["batches"][0]["response"][0]["success"].should eq ["true"]
55 | res["batches"][0]["response"][0]["created"].should eq ["false"]
56 | res = @api.query("Account", "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
57 | res["batches"][0]["response"][0]["Website"][0].should eq({"xsi:nil" => "true"})
58 | res["batches"][0]["response"][0]["Phone"][0].should eq({"xsi:nil" => "true"})
59 | end
60 | end
61 |
62 | context "when passed send_nulls = true and an array of fields not to null" do
63 | it "sets the nil and empty attributes to NULL, except for those included in the list of fields to ignore" do
64 | @api.update("Account", [{Id: @account_id, Website: "abc123", Phone: "5678"}], true)
65 | res = @api.query("Account", "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
66 | res["batches"][0]["response"][0]["Website"][0].should eq "abc123"
67 | res["batches"][0]["response"][0]["Phone"][0].should eq "5678"
68 | res = @api.upsert("Account", [{Id: @account_id, Website: "", Phone: nil}], "Id", true, true, [:Website, :Phone])
69 | res["batches"][0]["response"][0]["id"][0].should start_with(@account_id)
70 | res["batches"][0]["response"][0]["success"].should eq ["true"]
71 | res["batches"][0]["response"][0]["created"].should eq ["false"]
72 | res = @api.query("Account", "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
73 | res["batches"][0]["response"][0]["Website"][0].should eq("abc123")
74 | res["batches"][0]["response"][0]["Phone"][0].should eq("5678")
75 | end
76 | end
77 | end
78 |
79 | describe "update" do
80 | context "when there is not an error" do
81 | context "when not passed get_result" do
82 | it "doesnt return the batches array" do
83 | res = @api.update("Account", [{Id: @account_id, Website: "www.test.com"}])
84 | res["batches"].should be_nil
85 | end
86 | end
87 |
88 | context "when passed get_result = true" do
89 | it "returns the batches array" do
90 | res = @api.update("Account", [{Id: @account_id, Website: "www.test.com"}], true)
91 | res["batches"][0]["response"].is_a? Array
92 | res["batches"][0]["response"][0]["id"][0].should start_with(@account_id)
93 | res["batches"][0]["response"][0]["success"].should eq ["true"]
94 | res["batches"][0]["response"][0]["created"].should eq ["false"]
95 | end
96 | end
97 | end
98 |
99 | context "when there is an error" do
100 | context "when not passed get_result" do
101 | it "doesn't return the results array" do
102 | res = @api.update("Account", [{Id: @account_id, Website: "www.test.com"}, {Id: "abc123", Website: "www.test.com"}])
103 | res["batches"].should be_nil
104 | end
105 | end
106 |
107 | context "when passed get_result = true with batches" do
108 | it "returns the results array" do
109 | 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)
110 |
111 | res["batches"][0]["response"][0]["id"][0].should start_with(@account_id)
112 | res["batches"][0]["response"][0]["success"].should eq ["true"]
113 | res["batches"][0]["response"][0]["created"].should eq ["false"]
114 | res["batches"][0]["response"][1]["id"][0].should start_with(@account_id)
115 | res["batches"][0]["response"][1]["success"].should eq ["true"]
116 | res["batches"][0]["response"][1]["created"].should eq ["false"]
117 |
118 | res["batches"][1]["response"][0]["id"][0].should start_with(@account_id)
119 | res["batches"][1]["response"][0]["success"].should eq ["true"]
120 | res["batches"][1]["response"][0]["created"].should eq ["false"]
121 | 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"]})
122 | end
123 | end
124 | end
125 | end
126 |
127 | describe "create" do
128 | pending
129 | end
130 |
131 | describe "delete" do
132 | pending
133 | end
134 |
135 | describe "query" do
136 | context "when there are results" do
137 | it "returns the query results" do
138 | res = @api.query("Account", "SELECT id, Name From Account WHERE Name LIKE 'Test%'")
139 | res["batches"][0]["response"].length.should > 1
140 | res["batches"][0]["response"][0]["Id"].should_not be_nil
141 | end
142 |
143 | context "and there are multiple batches" do
144 | # need dev to create > 10k records in dev organization
145 | it "returns the query results in a merged hash"
146 | end
147 | end
148 |
149 | context "when there are no results" do
150 | it "returns nil" do
151 | res = @api.query("Account", "SELECT id From Account WHERE Name = 'ABC'")
152 | res["batches"][0]["response"].should eq nil
153 | end
154 | end
155 |
156 | context "when there is an error" do
157 | it "returns nil" do
158 | res = @api.query("Account", "SELECT id From Account WHERE Name = ''ABC'")
159 | res["batches"][0]["response"].should eq nil
160 | end
161 | end
162 | end
163 |
164 | describe "counters" do
165 | context "when read operations are called" do
166 | it "increments operation count and http GET count" do
167 | @api.counters[:http_get].should eq 0
168 | @api.counters[:query].should eq 0
169 | @api.query("Account", "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
170 | @api.counters[:http_get].should eq 1
171 | @api.counters[:query].should eq 1
172 | end
173 | end
174 |
175 | context "when update operations are called" do
176 | it "increments operation count and http POST count" do
177 | @api.counters[:http_post].should eq 0
178 | @api.counters[:update].should eq 0
179 | @api.update("Account", [{Id: @account_id, Website: "abc123", Phone: "5678"}], true)
180 | @api.counters[:http_post].should eq 1
181 | @api.counters[:update].should eq 1
182 | end
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "rubygems"
2 | require "bundler/setup"
3 | require "salesforce_bulk_api"
4 |
5 | RSpec.configure do |c|
6 | c.filter_run focus: true
7 | c.run_all_when_everything_filtered = true
8 | end
9 |
--------------------------------------------------------------------------------