restforce 2.5.4 → 4.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +56 -0
- data/.rubocop.yml +27 -14
- data/.rubocop_todo.yml +128 -81
- data/CHANGELOG.md +37 -3
- data/CONTRIBUTING.md +3 -3
- data/Gemfile +4 -2
- data/Guardfile +3 -1
- data/LICENSE +1 -1
- data/README.md +120 -19
- data/Rakefile +2 -1
- data/lib/restforce.rb +23 -1
- data/lib/restforce/abstract_client.rb +3 -0
- data/lib/restforce/attachment.rb +3 -0
- data/lib/restforce/client.rb +2 -0
- data/lib/restforce/collection.rb +3 -1
- data/lib/restforce/concerns/api.rb +20 -14
- data/lib/restforce/concerns/authentication.rb +2 -0
- data/lib/restforce/concerns/base.rb +2 -0
- data/lib/restforce/concerns/batch_api.rb +87 -0
- data/lib/restforce/concerns/caching.rb +4 -2
- data/lib/restforce/concerns/canvas.rb +3 -0
- data/lib/restforce/concerns/connection.rb +26 -20
- data/lib/restforce/concerns/picklists.rb +9 -6
- data/lib/restforce/concerns/streaming.rb +60 -1
- data/lib/restforce/concerns/verbs.rb +3 -1
- data/lib/restforce/config.rb +4 -1
- data/lib/restforce/data/client.rb +2 -0
- data/lib/restforce/document.rb +3 -0
- data/lib/restforce/mash.rb +2 -0
- data/lib/restforce/middleware.rb +2 -0
- data/lib/restforce/middleware/authentication.rb +8 -6
- data/lib/restforce/middleware/authentication/password.rb +2 -0
- data/lib/restforce/middleware/authentication/token.rb +2 -0
- data/lib/restforce/middleware/authorization.rb +3 -1
- data/lib/restforce/middleware/caching.rb +3 -1
- data/lib/restforce/middleware/custom_headers.rb +2 -0
- data/lib/restforce/middleware/gzip.rb +5 -3
- data/lib/restforce/middleware/instance_url.rb +7 -3
- data/lib/restforce/middleware/logger.rb +2 -0
- data/lib/restforce/middleware/mashify.rb +2 -0
- data/lib/restforce/middleware/multipart.rb +8 -4
- data/lib/restforce/middleware/raise_error.rb +26 -8
- data/lib/restforce/patches/parts.rb +2 -0
- data/lib/restforce/signed_request.rb +3 -0
- data/lib/restforce/sobject.rb +3 -0
- data/lib/restforce/tooling/client.rb +5 -3
- data/lib/restforce/upload_io.rb +2 -0
- data/lib/restforce/version.rb +3 -1
- data/restforce.gemspec +19 -12
- data/spec/fixtures/sobject/sobject_describe_success_response.json +48 -1
- data/spec/integration/abstract_client_spec.rb +51 -7
- data/spec/integration/data/client_spec.rb +24 -5
- data/spec/spec_helper.rb +2 -0
- data/spec/support/client_integration.rb +2 -0
- data/spec/support/concerns.rb +2 -0
- data/spec/support/event_machine.rb +2 -0
- data/spec/support/fixture_helpers.rb +4 -2
- data/spec/support/matchers.rb +2 -0
- data/spec/support/middleware.rb +3 -1
- data/spec/support/mock_cache.rb +4 -2
- data/spec/unit/abstract_client_spec.rb +2 -0
- data/spec/unit/attachment_spec.rb +2 -0
- data/spec/unit/collection_spec.rb +5 -3
- data/spec/unit/concerns/api_spec.rb +40 -11
- data/spec/unit/concerns/authentication_spec.rb +4 -2
- data/spec/unit/concerns/base_spec.rb +2 -0
- data/spec/unit/concerns/batch_api_spec.rb +107 -0
- data/spec/unit/concerns/caching_spec.rb +2 -0
- data/spec/unit/concerns/canvas_spec.rb +3 -1
- data/spec/unit/concerns/connection_spec.rb +5 -3
- data/spec/unit/concerns/streaming_spec.rb +115 -1
- data/spec/unit/config_spec.rb +10 -8
- data/spec/unit/data/client_spec.rb +2 -0
- data/spec/unit/document_spec.rb +2 -0
- data/spec/unit/mash_spec.rb +3 -1
- data/spec/unit/middleware/authentication/password_spec.rb +2 -0
- data/spec/unit/middleware/authentication/token_spec.rb +2 -0
- data/spec/unit/middleware/authentication_spec.rb +3 -1
- data/spec/unit/middleware/authorization_spec.rb +2 -0
- data/spec/unit/middleware/custom_headers_spec.rb +3 -1
- data/spec/unit/middleware/gzip_spec.rb +4 -2
- data/spec/unit/middleware/instance_url_spec.rb +2 -0
- data/spec/unit/middleware/logger_spec.rb +2 -0
- data/spec/unit/middleware/mashify_spec.rb +3 -1
- data/spec/unit/middleware/raise_error_spec.rb +34 -11
- data/spec/unit/signed_request_spec.rb +2 -0
- data/spec/unit/sobject_spec.rb +5 -3
- data/spec/unit/tooling/client_spec.rb +2 -0
- metadata +38 -20
- data/.travis.yml +0 -16
- data/Gemfile.travis +0 -8
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'restforce/concerns/verbs'
|
4
|
+
|
5
|
+
module Restforce
|
6
|
+
module Concerns
|
7
|
+
module BatchAPI
|
8
|
+
extend Restforce::Concerns::Verbs
|
9
|
+
|
10
|
+
define_verbs :post
|
11
|
+
|
12
|
+
def batch(halt_on_error: false)
|
13
|
+
subrequests = Subrequests.new(options)
|
14
|
+
yield(subrequests)
|
15
|
+
subrequests.requests.each_slice(25).map do |requests|
|
16
|
+
properties = {
|
17
|
+
batchRequests: requests,
|
18
|
+
haltOnError: halt_on_error
|
19
|
+
}
|
20
|
+
response = api_post('composite/batch', properties.to_json)
|
21
|
+
body = response.body
|
22
|
+
results = body['results']
|
23
|
+
if halt_on_error && body['hasErrors']
|
24
|
+
last_error_index = results.rindex { |result| result['statusCode'] != 412 }
|
25
|
+
last_error = results[last_error_index]
|
26
|
+
raise BatchAPIError, last_error['result'][0]['errorCode']
|
27
|
+
end
|
28
|
+
results.map(&:compact)
|
29
|
+
end.flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
def batch!(&block)
|
33
|
+
batch(halt_on_error: true, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
class Subrequests
|
37
|
+
def initialize(options)
|
38
|
+
@options = options
|
39
|
+
@requests = []
|
40
|
+
end
|
41
|
+
attr_reader :options, :requests
|
42
|
+
|
43
|
+
def create(sobject, attrs)
|
44
|
+
requests << { method: 'POST', url: batch_api_path(sobject), richInput: attrs }
|
45
|
+
end
|
46
|
+
|
47
|
+
def update(sobject, attrs)
|
48
|
+
id = attrs.fetch(attrs.keys.find { |k, v| k.to_s.casecmp?('id') }, nil)
|
49
|
+
raise ArgumentError, 'Id field missing from attrs.' unless id
|
50
|
+
|
51
|
+
attrs_without_id = attrs.reject { |k, v| k.to_s.casecmp?('id') }
|
52
|
+
requests << {
|
53
|
+
method: 'PATCH',
|
54
|
+
url: batch_api_path("#{sobject}/#{id}"),
|
55
|
+
richInput: attrs_without_id
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def destroy(sobject, id)
|
60
|
+
requests << { method: 'DELETE', url: batch_api_path("#{sobject}/#{id}") }
|
61
|
+
end
|
62
|
+
|
63
|
+
def upsert(sobject, ext_field, attrs)
|
64
|
+
raise ArgumentError, 'External id field missing.' unless ext_field
|
65
|
+
|
66
|
+
ext_id = attrs.fetch(attrs.keys.find { |k, v|
|
67
|
+
k.to_s.casecmp?(ext_field.to_s)
|
68
|
+
}, nil)
|
69
|
+
raise ArgumentError, 'External id missing from attrs.' unless ext_id
|
70
|
+
|
71
|
+
attrs_without_ext_id = attrs.reject { |k, v| k.to_s.casecmp?(ext_field) }
|
72
|
+
requests << {
|
73
|
+
method: 'PATCH',
|
74
|
+
url: batch_api_path("#{sobject}/#{ext_field}/#{ext_id}"),
|
75
|
+
richInput: attrs_without_ext_id
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def batch_api_path(path)
|
82
|
+
"v#{options[:api_version]}/sobjects/#{path}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
module Concerns
|
3
5
|
module Caching
|
@@ -6,9 +8,9 @@ module Restforce
|
|
6
8
|
# block - A query/describe/etc.
|
7
9
|
#
|
8
10
|
# Returns the result of the block
|
9
|
-
def without_caching
|
11
|
+
def without_caching
|
10
12
|
options[:use_cache] = false
|
11
|
-
|
13
|
+
yield
|
12
14
|
ensure
|
13
15
|
options.delete(:use_cache)
|
14
16
|
end
|
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
module Concerns
|
3
5
|
module Canvas
|
4
6
|
def decode_signed_request(signed_request)
|
5
7
|
raise 'client_secret not set.' unless options[:client_secret]
|
8
|
+
|
6
9
|
SignedRequest.decode(signed_request, options[:client_secret])
|
7
10
|
end
|
8
11
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
module Concerns
|
3
5
|
module Connection
|
@@ -13,7 +15,7 @@ module Restforce
|
|
13
15
|
def middleware
|
14
16
|
connection.builder
|
15
17
|
end
|
16
|
-
|
18
|
+
alias builder middleware
|
17
19
|
|
18
20
|
private
|
19
21
|
|
@@ -22,40 +24,44 @@ module Restforce
|
|
22
24
|
@connection ||= Faraday.new(options[:instance_url],
|
23
25
|
connection_options) do |builder|
|
24
26
|
# Parses JSON into Hashie::Mash structures.
|
25
|
-
unless
|
26
|
-
builder.use
|
27
|
+
unless options[:mashify] == false
|
28
|
+
builder.use Restforce::Middleware::Mashify, self, options
|
27
29
|
end
|
28
30
|
|
29
31
|
# Handles multipart file uploads for blobs.
|
30
|
-
builder.use
|
32
|
+
builder.use Restforce::Middleware::Multipart
|
31
33
|
# Converts the request into JSON.
|
32
|
-
builder.request
|
34
|
+
builder.request :json
|
33
35
|
# Handles reauthentication for 403 responses.
|
34
36
|
if authentication_middleware
|
35
|
-
builder.use
|
37
|
+
builder.use authentication_middleware, self, options
|
36
38
|
end
|
37
39
|
# Sets the oauth token in the headers.
|
38
|
-
builder.use
|
40
|
+
builder.use Restforce::Middleware::Authorization, self, options
|
39
41
|
# Ensures the instance url is set.
|
40
|
-
builder.use
|
42
|
+
builder.use Restforce::Middleware::InstanceURL, self, options
|
41
43
|
# Caches GET requests.
|
42
|
-
builder.use
|
44
|
+
builder.use Restforce::Middleware::Caching, cache, options if cache
|
43
45
|
# Follows 30x redirects.
|
44
|
-
builder.use
|
46
|
+
builder.use FaradayMiddleware::FollowRedirects
|
45
47
|
# Raises errors for 40x responses.
|
46
|
-
builder.use
|
48
|
+
builder.use Restforce::Middleware::RaiseError
|
47
49
|
# Parses returned JSON response into a hash.
|
48
50
|
builder.response :json, content_type: /\bjson$/
|
49
51
|
# Compress/Decompress the request/response
|
50
|
-
|
52
|
+
unless adapter == :httpclient
|
53
|
+
builder.use Restforce::Middleware::Gzip, self, options
|
54
|
+
end
|
51
55
|
# Inject custom headers into requests
|
52
|
-
builder.use
|
56
|
+
builder.use Restforce::Middleware::CustomHeaders, self, options
|
53
57
|
# Log request/responses
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
if Restforce.log?
|
59
|
+
builder.use Restforce::Middleware::Logger,
|
60
|
+
Restforce.configuration.logger,
|
61
|
+
options
|
62
|
+
end
|
57
63
|
|
58
|
-
builder.adapter
|
64
|
+
builder.adapter adapter
|
59
65
|
end
|
60
66
|
end
|
61
67
|
|
@@ -67,10 +73,10 @@ module Restforce
|
|
67
73
|
def connection_options
|
68
74
|
{ request: {
|
69
75
|
timeout: options[:timeout],
|
70
|
-
open_timeout: options[:timeout]
|
76
|
+
open_timeout: options[:timeout]
|
77
|
+
},
|
71
78
|
proxy: options[:proxy_uri],
|
72
|
-
ssl: options[:ssl]
|
73
|
-
}
|
79
|
+
ssl: options[:ssl] }
|
74
80
|
end
|
75
81
|
|
76
82
|
# Internal: Returns true if the middlware stack includes the
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
module Concerns
|
3
5
|
module Picklists
|
@@ -26,14 +28,13 @@ module Restforce
|
|
26
28
|
PicklistValues.new(describe(sobject)['fields'], field, options)
|
27
29
|
end
|
28
30
|
|
29
|
-
private
|
30
|
-
|
31
31
|
class PicklistValues < Array
|
32
32
|
def initialize(fields, field, options = {})
|
33
33
|
@fields = fields
|
34
34
|
@field = field
|
35
35
|
@valid_for = options.delete(:valid_for)
|
36
36
|
raise "#{field} is not a dependent picklist" if @valid_for && !dependent?
|
37
|
+
|
37
38
|
replace(picklist_values)
|
38
39
|
end
|
39
40
|
|
@@ -60,7 +61,9 @@ module Restforce
|
|
60
61
|
|
61
62
|
def controlling_picklist
|
62
63
|
@_controlling_picklist ||= controlling_field['picklistValues'].
|
63
|
-
|
64
|
+
find do |picklist_entry|
|
65
|
+
picklist_entry['value'] == @valid_for
|
66
|
+
end
|
64
67
|
end
|
65
68
|
|
66
69
|
def index
|
@@ -80,9 +83,9 @@ module Restforce
|
|
80
83
|
# See http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_des
|
81
84
|
# cribesobjects_describesobjectresult.htm
|
82
85
|
def valid?(picklist_entry)
|
83
|
-
valid_for = picklist_entry['validFor'].ljust(16, 'A').
|
84
|
-
|
85
|
-
(valid_for[index >> 3] & (0x80 >> index % 8))
|
86
|
+
valid_for = picklist_entry['validFor'].ljust(16, 'A').unpack1('m').
|
87
|
+
unpack('C*')
|
88
|
+
(valid_for[index >> 3] & (0x80 >> index % 8)).positive?
|
86
89
|
end
|
87
90
|
end
|
88
91
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
module Concerns
|
3
5
|
module Streaming
|
@@ -7,7 +9,8 @@ module Restforce
|
|
7
9
|
# block - A block to run when a new message is received.
|
8
10
|
#
|
9
11
|
# Returns a Faye::Subscription
|
10
|
-
def subscribe(channels, &block)
|
12
|
+
def subscribe(channels, options = {}, &block)
|
13
|
+
Array(channels).each { |channel| replay_handlers[channel] = options[:replay] }
|
11
14
|
faye.subscribe Array(channels).map { |channel| "/topic/#{channel}" }, &block
|
12
15
|
end
|
13
16
|
|
@@ -30,6 +33,62 @@ module Restforce
|
|
30
33
|
client.bind 'transport:up' do
|
31
34
|
Restforce.log "[COMETD UP]"
|
32
35
|
end
|
36
|
+
|
37
|
+
client.add_extension ReplayExtension.new(replay_handlers)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def replay_handlers
|
42
|
+
@_replay_handlers ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
class ReplayExtension
|
46
|
+
def initialize(replay_handlers)
|
47
|
+
@replay_handlers = replay_handlers
|
48
|
+
end
|
49
|
+
|
50
|
+
def incoming(message, callback)
|
51
|
+
callback.call(message).tap do
|
52
|
+
channel = message.fetch('channel').gsub('/topic/', '')
|
53
|
+
replay_id = message.fetch('data', {}).fetch('event', {})['replayId']
|
54
|
+
|
55
|
+
handler = @replay_handlers[channel]
|
56
|
+
if !replay_id.nil? && !handler.nil? && handler.respond_to?(:[]=)
|
57
|
+
# remember the last replay_id for this channel
|
58
|
+
handler[channel] = replay_id
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def outgoing(message, callback)
|
64
|
+
# Leave non-subscribe messages alone
|
65
|
+
return callback.call(message) unless message['channel'] == '/meta/subscribe'
|
66
|
+
|
67
|
+
channel = message['subscription'].gsub('/topic/', '')
|
68
|
+
|
69
|
+
# Set the replay value for the channel
|
70
|
+
message['ext'] ||= {}
|
71
|
+
message['ext']['replay'] = {
|
72
|
+
"/topic/#{channel}" => replay_id(channel)
|
73
|
+
}
|
74
|
+
|
75
|
+
# Carry on and send the message to the server
|
76
|
+
callback.call message
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def replay_id(channel)
|
82
|
+
handler = @replay_handlers[channel]
|
83
|
+
if handler.is_a?(Integer)
|
84
|
+
handler # treat it as a scalar
|
85
|
+
elsif handler.respond_to?(:[])
|
86
|
+
# Ask for the latest replayId for this channel
|
87
|
+
handler[channel]
|
88
|
+
else
|
89
|
+
# Just pass it along
|
90
|
+
handler
|
91
|
+
end
|
33
92
|
end
|
34
93
|
end
|
35
94
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
module Concerns
|
3
5
|
module Verbs
|
@@ -34,7 +36,7 @@ module Restforce
|
|
34
36
|
begin
|
35
37
|
connection.send(verb, *args, &block)
|
36
38
|
rescue Restforce::UnauthorizedError
|
37
|
-
if retries
|
39
|
+
if retries.positive?
|
38
40
|
retries -= 1
|
39
41
|
connection.url_prefix = options[:instance_url]
|
40
42
|
retry
|
data/lib/restforce/config.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'logger'
|
2
4
|
|
3
5
|
module Restforce
|
@@ -32,6 +34,7 @@ module Restforce
|
|
32
34
|
|
33
35
|
def log(message)
|
34
36
|
return unless Restforce.log?
|
37
|
+
|
35
38
|
configuration.logger.send(configuration.log_level, message)
|
36
39
|
end
|
37
40
|
end
|
@@ -60,7 +63,7 @@ module Restforce
|
|
60
63
|
private
|
61
64
|
|
62
65
|
attr_reader :default
|
63
|
-
|
66
|
+
alias default_provided? default
|
64
67
|
|
65
68
|
def write_attribute
|
66
69
|
configuration.send :attr_accessor, name
|
data/lib/restforce/document.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
class Document < Restforce::SObject
|
3
5
|
# Public: Returns the body of the document.
|
@@ -15,6 +17,7 @@ module Restforce
|
|
15
17
|
|
16
18
|
def ensure_body
|
17
19
|
return true if self.Body?
|
20
|
+
|
18
21
|
raise 'You need to query the Body for the record first.'
|
19
22
|
end
|
20
23
|
end
|
data/lib/restforce/mash.rb
CHANGED
data/lib/restforce/middleware.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Restforce
|
2
4
|
# Faraday middleware that allows for on the fly authentication of requests.
|
3
5
|
# When a request fails (a status of 401 is returned), the middleware
|
@@ -31,9 +33,7 @@ module Restforce
|
|
31
33
|
@options[:instance_url] = response.body['instance_url']
|
32
34
|
@options[:oauth_token] = response.body['access_token']
|
33
35
|
|
34
|
-
|
35
|
-
@options[:authentication_callback].call(response.body)
|
36
|
-
end
|
36
|
+
@options[:authentication_callback]&.call(response.body)
|
37
37
|
|
38
38
|
response.body
|
39
39
|
end
|
@@ -50,9 +50,11 @@ module Restforce
|
|
50
50
|
builder.use Restforce::Middleware::Mashify, nil, @options
|
51
51
|
builder.response :json
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
if Restforce.log?
|
54
|
+
builder.use Restforce::Middleware::Logger,
|
55
|
+
Restforce.configuration.logger,
|
56
|
+
@options
|
57
|
+
end
|
56
58
|
|
57
59
|
builder.adapter @options[:adapter]
|
58
60
|
end
|