salesforce_chunker 1.1.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
+ SHA256:
3
+ metadata.gz: 63428f8b047e970e10dab25118c83b9445e6c79b7d75cf3fd6552a37d8379cd5
4
+ data.tar.gz: 543260b73b9c91bed55e7788527553419bc83459d7917119e6b2286d74880a71
5
+ SHA512:
6
+ metadata.gz: 918d7303b522c79d351901073ac415fdb273b1e3582a9c6565cdb4878e76fb0744023766b6cacf7792a30f840f9bfe52c989790ceb7dad8e5de080503f34ef22
7
+ data.tar.gz: 28883b66685b1071a31b826ab23f166f4ec6dd42a86f98d5ae50ce590c3a38bb43ce6a374170408f522835edef009c20cdb01e5198d5091ba7e4c366eb95f7ca
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.16.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # CHANGELOG
2
+
3
+ ## 1.1.0 - 2018-11-06
4
+
5
+ - Added ManualChunkingQuery, which implements chunking within the gem for any Salesforce field.
6
+
7
+ ## 1.0.0 - 2018-09-12
8
+
9
+ - Initial Open Source Release
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in salesforce_chunker.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,36 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ salesforce_chunker (1.1.0)
5
+ httparty (~> 0.13)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ coderay (1.1.2)
11
+ httparty (0.16.2)
12
+ multi_xml (>= 0.5.2)
13
+ metaclass (0.0.4)
14
+ method_source (0.9.0)
15
+ minitest (5.11.3)
16
+ mocha (1.5.0)
17
+ metaclass (~> 0.0.1)
18
+ multi_xml (0.6.0)
19
+ pry (0.11.3)
20
+ coderay (~> 1.1.0)
21
+ method_source (~> 0.9.0)
22
+ rake (10.5.0)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ bundler (~> 1.16)
29
+ minitest (~> 5.0)
30
+ mocha (~> 1.5.0)
31
+ pry (~> 0.11.1)
32
+ rake (~> 10.0)
33
+ salesforce_chunker!
34
+
35
+ BUNDLED WITH
36
+ 1.16.6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Shopify
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # SalesforceChunker
2
+
3
+ The `salesforce_chunker` gem is a ruby library for interacting with the Salesforce Bulk API. It was primarily designed as an extractor to handle queries using batching and [Primary Key Chunking](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/async_api_headers_enable_pk_chunking.htm).
4
+
5
+ Currently, only querying is built into `SalesforceChunker::Client`, but non-query jobs can be created with `SalesforceChunker::Job`.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'salesforce_chunker'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install salesforce_chunker
22
+
23
+ ## Usage
24
+
25
+ ### SalesforceChunker::Client
26
+
27
+ #### Simple Example
28
+
29
+ ```ruby
30
+ client = SalesforceChunker::Client.new(
31
+ username: "username",
32
+ password: "password",
33
+ security_token: "security_token",
34
+ )
35
+
36
+ names = client.query(query: "Select Name From User", object: "User").map { |result| result["Name"] }
37
+ ```
38
+
39
+ #### Initialize
40
+
41
+ ```ruby
42
+ client = SalesforceChunker::Client.new(
43
+ username: "username",
44
+ password: "password",
45
+ security_token: "security_token",
46
+ domain: "login",
47
+ salesforce_version: "42.0",
48
+ )
49
+ ```
50
+
51
+ | Parameter | |
52
+ | --- | --- |
53
+ | username | required |
54
+ | password | required |
55
+ | security_token | may be required depending on your Salesforce setup |
56
+ | domain | optional. defaults to `"login"`. |
57
+ | salesforce_version | optional. defaults to `"42.0"`. Must be >= `"33.0"` to use PK Chunking. |
58
+
59
+ #### Functions
60
+
61
+ | function | |
62
+ | --- | --- |
63
+ | query |
64
+ | single_batch_query | calls `query(job_type: "single_batch", **options)` |
65
+ | primary_key_chunking_query | calls `query(job_type: "primary_key_chunking", **options)` |
66
+
67
+ #### Query
68
+
69
+ ```ruby
70
+ options = {
71
+ query: "Select Name from Account",
72
+ object: "Account",
73
+ batch_size: 100000,
74
+ retry_seconds: 10,
75
+ timeout_seconds: 3600,
76
+ logger: nil,
77
+ log_output: STDOUT,
78
+ job_type: "primary_key_chunking",
79
+ }
80
+
81
+ client.query(options) do |result|
82
+ process(result)
83
+ end
84
+ ```
85
+
86
+ | Parameter | | |
87
+ | --- | --- | --- |
88
+ | query | required | SOQL query. |
89
+ | object | required | Salesforce Object type. |
90
+ | batch_size | optional | defaults to `100000`. Number of records to process in a batch. (Only for PK Chunking) |
91
+ | retry_seconds | optional | defaults to `10`. Number of seconds to wait before querying API for updated results. |
92
+ | timeout_seconds | optional | defaults to `3600`. Number of seconds to wait before query is killed. |
93
+ | logger | optional | logger to use. Must be instance of or similar to rails logger. |
94
+ | log_output | optional | log output to use. i.e. `STDOUT`. |
95
+ | job_type | optional | defaults to `"primary_key_chunking"`. Can also be set to `"single_batch"`. |
96
+
97
+ `query` can either be called with a block, or will return an enumerator:
98
+
99
+ ```ruby
100
+ names = client.query(query, object, options).map { |result| result["Name"] }
101
+ ```
102
+
103
+ ### Under the hood: SalesforceChunker::Job
104
+
105
+ Using `SalesforceChunker::Job`, you have more direct access to the Salesforce Bulk API functions, such as `create_batch`, `get_batch_statuses`, and `retrieve_batch_results`. This can be used to perform custom tasks, such as upserts or multiple batch queries.
106
+
107
+ This should be used in coordination with `SalesforceChunker::Connection`, which has the same initialization process as `SalesforceChunker::Client`.
108
+
109
+ ```ruby
110
+ connection = SalesforceChunker::Connection.new(
111
+ username: "username",
112
+ password: "password",
113
+ security_token: "security_token",
114
+ )
115
+
116
+ job = SalesforceChunker::Job.new(
117
+ connection: connection,
118
+ object: "Account",
119
+ operation: "query",
120
+ log_output: STDOUT,
121
+ )
122
+
123
+ job.create_batch("Select Id From Account Order By Id Desc Limit 1")
124
+ job.create_batch("Select Id From Account Order By Id Asc Limit 1")
125
+ job.close
126
+
127
+ job.instance_variable_set(:@batches_count, 2)
128
+ ids = job.download_results.to_a
129
+ ```
130
+
131
+ Also, `SalesforceChunker::SingleBatchJob` can be used to create a Job with only a single batch. This automatically handles the batch creation, closing, and setting `@batches_count`.
132
+
133
+ ```ruby
134
+ job = SalesforceChunker::SingleBatchJob.new(
135
+ connection: connection,
136
+ object: "Account",
137
+ operation: "upsert",
138
+ payload: [{ "Name" => "Random Account", "IdField__c" => "123456" }],
139
+ external_id: "IdField__c",
140
+ log_output: STDOUT,
141
+ )
142
+
143
+ loop do
144
+ batch = job.get_batch_statuses.first
145
+ if batch["state"] == "Completed"
146
+ break
147
+ elsif batch["state"] == "Failed"
148
+ raise "batch failed"
149
+ end
150
+ sleep 5
151
+ end
152
+ ```
153
+
154
+ ## Development
155
+
156
+ After checking out the repo,
157
+ - run `bin/setup` to install dependencies.
158
+ - run `rake test` to run the tests.
159
+ - run `bin/console` for an interactive prompt that will allow you to experiment.
160
+
161
+ ## Contributing
162
+
163
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/salesforce_chunker.
164
+
165
+ ## License
166
+
167
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "salesforce_chunker"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dev.yml ADDED
@@ -0,0 +1,16 @@
1
+ # For internal Shopify employee use
2
+ # the requirements below describe the required dependencies
3
+ ---
4
+ name: salesforce-chunker
5
+
6
+ up:
7
+ - ruby: 2.5.0
8
+ - bundler
9
+
10
+ commands:
11
+ console:
12
+ desc: 'start a rails console'
13
+ run: bin/console
14
+ test:
15
+ desc: 'run the tests'
16
+ run: bundle exec rake test
@@ -0,0 +1,76 @@
1
+ require "httparty"
2
+
3
+ module SalesforceChunker
4
+ class Connection
5
+
6
+ def initialize(username: "", password: "", security_token: "", domain: "login", salesforce_version: "42.0", **options)
7
+ @log = options[:logger] || Logger.new(options[:log_output])
8
+ @log.progname = "salesforce_chunker"
9
+
10
+ response = HTTParty.post(
11
+ "https://#{domain}.salesforce.com/services/Soap/u/#{salesforce_version}",
12
+ headers: { "SOAPAction": "login", "Content-Type": "text/xml; charset=UTF-8" },
13
+ body: self.class.login_soap_request_body(username, password, security_token)
14
+ ).parsed_response
15
+
16
+ result = response["Envelope"]["Body"]["loginResponse"]["result"]
17
+
18
+ instance = self.class.get_instance(result["serverUrl"])
19
+
20
+ @base_url = "https://#{instance}.salesforce.com/services/async/#{salesforce_version}/"
21
+ @default_headers = {
22
+ "Content-Type": "application/json",
23
+ "X-SFDC-Session": result["sessionId"],
24
+ "Accept-Encoding": "gzip",
25
+ }
26
+ rescue NoMethodError
27
+ raise ConnectionError, response["Envelope"]["Body"]["Fault"]["faultstring"]
28
+ end
29
+
30
+ def post_json(url, body, headers={})
31
+ post(url, body.to_json, headers)
32
+ end
33
+
34
+ def post(url, body, headers={})
35
+ @log.info "POST: #{url}"
36
+ response = HTTParty.post(@base_url + url, headers: @default_headers.merge(headers), body: body)
37
+ self.class.check_response_error(response.parsed_response)
38
+ end
39
+
40
+ def get_json(url, headers={})
41
+ @log.info "GET: #{url}"
42
+ response = HTTParty.get(@base_url + url, headers: @default_headers.merge(headers))
43
+ self.class.check_response_error(response.parsed_response)
44
+ end
45
+
46
+ private
47
+
48
+ def self.login_soap_request_body(username, password, security_token)
49
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>
50
+ <env:Envelope
51
+ xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
52
+ xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
53
+ xmlns:env=\"http://schemas.xmlsoap.org/soap/envelope/\"
54
+ xmlns:urn=\"urn:partner.soap.sforce.com\">
55
+ <env:Body>
56
+ <n1:login xmlns:n1=\"urn:partner.soap.sforce.com\">
57
+ <n1:username>#{username.encode(xml: :text)}</n1:username>
58
+ <n1:password>#{password.encode(xml: :text)}#{security_token.encode(xml: :text)}</n1:password>
59
+ </n1:login>
60
+ </env:Body>
61
+ </env:Envelope>"
62
+ end
63
+
64
+ def self.get_instance(server_url)
65
+ /https:\/\/(.*).salesforce.com/.match(server_url)[1]
66
+ end
67
+
68
+ def self.check_response_error(response)
69
+ if response.is_a?(Hash) && response.key?("exceptionCode")
70
+ raise ResponseError, "#{response["exceptionCode"]}: #{response["exceptionMessage"]}"
71
+ else
72
+ response
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,18 @@
1
+ module SalesforceChunker
2
+ class Error < StandardError; end
3
+
4
+ # Raised when connecting with Salesforce fails
5
+ class ConnectionError < Error; end
6
+
7
+ # Raised when a request sent to Salesforce is invalid
8
+ class ResponseError < Error; end
9
+
10
+ # Raised when Salesforce returns a failed batch
11
+ class BatchError < Error; end
12
+
13
+ # Raised when Salesforce returns a successful batch with failed record(s)
14
+ class RecordError < Error; end
15
+
16
+ # Raised when batch job exceeds time limit
17
+ class TimeoutError < Error; end
18
+ end
@@ -0,0 +1,105 @@
1
+ module SalesforceChunker
2
+ class Job
3
+ attr_reader :batches_count
4
+
5
+ QUERY_OPERATIONS = ["query", "queryall"].freeze
6
+ DEFAULT_RETRY_SECONDS = 10
7
+ DEFAULT_TIMEOUT_SECONDS = 3600
8
+
9
+ def initialize(connection:, object:, operation:, **options)
10
+ @log = options[:logger] || Logger.new(options[:log_output])
11
+ @log.progname = "salesforce_chunker"
12
+
13
+ @connection = connection
14
+ @operation = operation
15
+ @batches_count = nil
16
+
17
+ @log.info "Creating Bulk API Job"
18
+ @job_id = create_job(object, options.slice(:headers, :external_id))
19
+ end
20
+
21
+ def download_results(**options)
22
+ return nil unless QUERY_OPERATIONS.include?(@operation)
23
+ return to_enum(:download_results, **options) unless block_given?
24
+
25
+ retry_seconds = options[:retry_seconds] || DEFAULT_RETRY_SECONDS
26
+ timeout_at = Time.now.utc + (options[:timeout_seconds] || DEFAULT_TIMEOUT_SECONDS)
27
+ downloaded_batches = []
28
+
29
+ loop do
30
+ @log.info "Retrieving batch status information"
31
+ get_completed_batches.each do |batch|
32
+ next if downloaded_batches.include?(batch["id"])
33
+ @log.info "Batch #{downloaded_batches.length + 1} of #{@batches_count || '?'}: " \
34
+ "retrieving #{batch["numberRecordsProcessed"]} records"
35
+ get_batch_results(batch["id"]) { |result| yield(result) } if batch["numberRecordsProcessed"] > 0
36
+ downloaded_batches.append(batch["id"])
37
+ end
38
+
39
+ break if @batches_count && downloaded_batches.length == @batches_count
40
+ raise TimeoutError, "Timeout during batch processing" if Time.now.utc > timeout_at
41
+
42
+ @log.info "Waiting #{retry_seconds} seconds"
43
+ sleep(retry_seconds)
44
+ end
45
+
46
+ @log.info "Completed"
47
+ end
48
+
49
+ def get_completed_batches
50
+ get_batch_statuses.select do |batch|
51
+ raise BatchError, "Batch failed: #{batch["stateMessage"]}" if batch["state"] == "Failed"
52
+ raise RecordError, "Failed records in batch" if batch["state"] == "Completed" && batch["numberRecordsFailed"] > 0
53
+ batch["state"] == "Completed"
54
+ end
55
+ end
56
+
57
+ def get_batch_results(batch_id)
58
+ retrieve_batch_results(batch_id).each do |result_id|
59
+ retrieve_results(batch_id, result_id).each do |result|
60
+ result.tap { |h| h.delete("attributes") }
61
+ yield(result)
62
+ end
63
+ end
64
+ end
65
+
66
+ def create_batch(payload)
67
+ if QUERY_OPERATIONS.include?(@operation)
68
+ @log.info "Creating #{@operation.capitalize} Batch: \"#{payload.gsub(/\n/, " ").strip}\""
69
+ @connection.post("job/#{@job_id}/batch", payload.to_s)["id"]
70
+ else
71
+ @log.info "Creating #{@operation.capitalize} Batch"
72
+ @connection.post_json("job/#{@job_id}/batch", payload)["id"]
73
+ end
74
+ end
75
+
76
+ def get_batch_statuses
77
+ @connection.get_json("job/#{@job_id}/batch")["batchInfo"]
78
+ end
79
+
80
+ def retrieve_batch_results(batch_id)
81
+ @connection.get_json("job/#{@job_id}/batch/#{batch_id}/result")
82
+ end
83
+
84
+ def retrieve_results(batch_id, result_id)
85
+ @connection.get_json("job/#{@job_id}/batch/#{batch_id}/result/#{result_id}")
86
+ end
87
+
88
+ def close
89
+ body = {"state": "Closed"}
90
+ @connection.post_json("job/#{@job_id}/", body)
91
+ end
92
+
93
+ private
94
+
95
+ def create_job(object, options)
96
+ body = {
97
+ "operation": @operation,
98
+ "object": object,
99
+ "contentType": "JSON",
100
+ }
101
+ body[:externalIdFieldName] = options[:external_id] if @operation == "upsert"
102
+ @connection.post_json("job", body, options[:headers].to_h)["id"]
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,54 @@
1
+ module SalesforceChunker
2
+ class ManualChunkingQuery < Job
3
+
4
+ def initialize(connection:, object:, operation:, query:, **options)
5
+ batch_size = options[:batch_size] || 100000
6
+ where_clause = self.class.query_where_clause(query)
7
+
8
+ super(connection: connection, object: object, operation: operation, **options)
9
+ @log.info "Using Manual Chunking"
10
+
11
+ @log.info "Retrieving Ids from records"
12
+ breakpoints = breakpoints(object, where_clause, batch_size)
13
+
14
+ @log.info "Creating Query Batches"
15
+ create_batches(query, breakpoints, where_clause)
16
+
17
+ close
18
+ end
19
+
20
+ def get_batch_statuses
21
+ batches = super
22
+ batches.delete_if { |batch| batch["id"] == @initial_batch_id && batches.count > 1 }
23
+ end
24
+
25
+ def breakpoints(object, where_clause, batch_size)
26
+ @batches_count = 1
27
+ @initial_batch_id = create_batch("Select Id From #{object} #{where_clause} Order By Id Asc")
28
+
29
+ download_results(retry_seconds: 10)
30
+ .with_index
31
+ .select { |_, i| i % batch_size == 0 && i != 0 }
32
+ .map { |result, _| result["Id"] }
33
+ end
34
+
35
+ def create_batches(query, breakpoints, where_clause)
36
+ if breakpoints.empty?
37
+ create_batch(query)
38
+ else
39
+ query += where_clause.empty? ? " Where" : " And"
40
+
41
+ create_batch("#{query} Id < '#{breakpoints.first}'")
42
+ breakpoints.each_cons(2) do |first, second|
43
+ create_batch("#{query} Id >= '#{first}' And Id < '#{second}'")
44
+ end
45
+ create_batch("#{query} Id >= '#{breakpoints.last}'")
46
+ end
47
+ @batches_count = breakpoints.length + 1
48
+ end
49
+
50
+ def self.query_where_clause(query)
51
+ query.partition(/where\s/i)[1..2].join
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ module SalesforceChunker
2
+ class PrimaryKeyChunkingQuery < Job
3
+
4
+ def initialize(connection:, object:, operation:, query:, **options)
5
+ batch_size = options[:batch_size] || 100000
6
+
7
+ if options[:headers].nil?
8
+ options[:headers] = {"Sforce-Enable-PKChunking": "true; chunkSize=#{batch_size};" }
9
+ else
10
+ options[:headers].reverse_merge!({"Sforce-Enable-PKChunking": "true; chunkSize=#{batch_size};" })
11
+ end
12
+
13
+ super(connection: connection, object: object, operation: operation, **options)
14
+ @log.info "Using Primary Key Chunking"
15
+ @initial_batch_id = create_batch(query)
16
+ end
17
+
18
+ def get_batch_statuses
19
+ batches = super
20
+ finalize_chunking_setup(batches) if @batches_count.nil?
21
+ batches
22
+ end
23
+
24
+ private
25
+
26
+ def finalize_chunking_setup(batches)
27
+ initial_batch = batches.select { |batch| batch["id"] == @initial_batch_id }.first
28
+ if initial_batch && initial_batch["state"] == "NotProcessed"
29
+ @batches_count = batches.length - 1
30
+ close
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ module SalesforceChunker
2
+ class SingleBatchJob < Job
3
+ def initialize(connection:, object:, operation:, **options)
4
+ super(connection: connection, object: object, operation: operation, **options)
5
+ payload = options[:payload] || options[:query]
6
+ @log.info "Using Single Batch"
7
+ @batch_id = create_batch(payload)
8
+ @batches_count = 1
9
+ close
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module SalesforceChunker
2
+ VERSION = "1.1.0"
3
+ end
@@ -0,0 +1,56 @@
1
+ require "salesforce_chunker/connection.rb"
2
+ require "salesforce_chunker/exceptions.rb"
3
+ require "salesforce_chunker/job.rb"
4
+ require "salesforce_chunker/single_batch_job.rb"
5
+ require "salesforce_chunker/primary_key_chunking_query.rb"
6
+ require "salesforce_chunker/manual_chunking_query.rb"
7
+ require 'logger'
8
+
9
+ module SalesforceChunker
10
+ class Client
11
+
12
+ def initialize(**options)
13
+ @log = options[:logger] || Logger.new(options[:log_output])
14
+ @log.progname = "salesforce_chunker"
15
+
16
+ @connection = SalesforceChunker::Connection.new(**options, logger: @log)
17
+ end
18
+
19
+ def query(query:, object:, **options)
20
+ return to_enum(:query, query: query, object: object, **options) unless block_given?
21
+
22
+ case options[:job_type]
23
+ when "single_batch"
24
+ job_class = SalesforceChunker::SingleBatchJob
25
+ when "manual_chunking"
26
+ job_class = SalesforceChunker::ManualChunkingQuery
27
+ when "primary_key_chunking", nil # for backwards compatibility
28
+ job_class = SalesforceChunker::PrimaryKeyChunkingQuery
29
+ end
30
+
31
+ job_params = {
32
+ connection: @connection,
33
+ object: object,
34
+ operation: "query",
35
+ query: query,
36
+ **options.slice(:batch_size, :logger, :log_output)
37
+ }
38
+ job_params[:logger] = @log if job_params[:logger].nil? && job_params[:log_output].nil?
39
+
40
+ job = job_class.new(**job_params)
41
+ job.download_results(**options.slice(:timeout, :retry_seconds)) { |result| yield(result) }
42
+ end
43
+
44
+ def single_batch_query(**options)
45
+ query(**options.merge(job_type: "single_batch"))
46
+ end
47
+
48
+ def primary_key_chunking_query(**options)
49
+ query(**options.merge(job_type: "primary_key_chunking"))
50
+ end
51
+
52
+ def manual_chunking_query(**options)
53
+ query(**options.merge(job_type: "manual_chunking"))
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "salesforce_chunker/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "salesforce_chunker"
8
+ spec.version = SalesforceChunker::VERSION
9
+ spec.authors = ["Curtis Holmes"]
10
+ spec.email = ["curtis.holmes@shopify.com"]
11
+
12
+ spec.summary = %q{Salesforce Bulk API Client}
13
+ spec.description = %q{Salesforce client and extractor designed for handling large amounts of data}
14
+ spec.homepage = 'https://github.com/Shopify/salesforce_chunker'
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "httparty", "~> 0.13"
25
+ spec.add_development_dependency "bundler", "~> 1.16"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "minitest", "~> 5.0"
28
+ spec.add_development_dependency "mocha", "~> 1.5.0"
29
+ spec.add_development_dependency "pry", "~> 0.11.1"
30
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salesforce_chunker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Curtis Holmes
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-11-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.13'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mocha
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.5.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.5.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.11.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.11.1
97
+ description: Salesforce client and extractor designed for handling large amounts of
98
+ data
99
+ email:
100
+ - curtis.holmes@shopify.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".travis.yml"
107
+ - CHANGELOG.md
108
+ - Gemfile
109
+ - Gemfile.lock
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - bin/console
114
+ - bin/setup
115
+ - dev.yml
116
+ - lib/salesforce_chunker.rb
117
+ - lib/salesforce_chunker/connection.rb
118
+ - lib/salesforce_chunker/exceptions.rb
119
+ - lib/salesforce_chunker/job.rb
120
+ - lib/salesforce_chunker/manual_chunking_query.rb
121
+ - lib/salesforce_chunker/primary_key_chunking_query.rb
122
+ - lib/salesforce_chunker/single_batch_job.rb
123
+ - lib/salesforce_chunker/version.rb
124
+ - salesforce_chunker.gemspec
125
+ homepage: https://github.com/Shopify/salesforce_chunker
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.7.6
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Salesforce Bulk API Client
149
+ test_files: []