restforce 2.5.4 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|