restforce 4.2.1 → 6.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/.github/ISSUE_TEMPLATE/unhandled-salesforce-error.md +17 -0
- data/.github/dependabot.yml +10 -0
- data/.github/funding.yml +1 -0
- data/.github/workflows/build.yml +23 -0
- data/.github/workflows/faraday.yml +27 -0
- data/.rubocop.yml +5 -4
- data/CHANGELOG.md +115 -0
- data/CONTRIBUTING.md +21 -1
- data/Dockerfile +31 -0
- data/Gemfile +15 -7
- data/README.md +102 -24
- data/UPGRADING.md +67 -0
- data/docker-compose.yml +7 -0
- data/lib/restforce/abstract_client.rb +1 -0
- data/lib/restforce/collection.rb +27 -4
- data/lib/restforce/concerns/api.rb +3 -2
- data/lib/restforce/concerns/base.rb +2 -2
- data/lib/restforce/concerns/caching.rb +7 -0
- data/lib/restforce/concerns/composite_api.rb +104 -0
- data/lib/restforce/concerns/connection.rb +1 -1
- data/lib/restforce/concerns/picklists.rb +2 -2
- data/lib/restforce/concerns/streaming.rb +1 -3
- data/lib/restforce/config.rb +14 -9
- data/lib/restforce/error_code.rb +650 -0
- data/lib/restforce/file_part.rb +32 -0
- data/lib/restforce/mash.rb +8 -3
- data/lib/restforce/middleware/authentication.rb +1 -0
- data/lib/restforce/middleware/caching.rb +140 -15
- data/lib/restforce/middleware/json_request.rb +90 -0
- data/lib/restforce/middleware/json_response.rb +85 -0
- data/lib/restforce/middleware/logger.rb +14 -9
- data/lib/restforce/middleware/raise_error.rb +13 -5
- data/lib/restforce/middleware.rb +2 -0
- data/lib/restforce/version.rb +1 -1
- data/lib/restforce.rb +15 -14
- data/restforce.gemspec +13 -21
- data/spec/fixtures/sobject/list_view_results_success_response.json +151 -0
- data/spec/integration/abstract_client_spec.rb +56 -35
- data/spec/integration/data/client_spec.rb +6 -2
- data/spec/spec_helper.rb +24 -1
- data/spec/support/client_integration.rb +7 -7
- data/spec/support/concerns.rb +1 -1
- data/spec/support/fixture_helpers.rb +1 -3
- data/spec/support/middleware.rb +1 -2
- data/spec/unit/collection_spec.rb +38 -2
- data/spec/unit/concerns/api_spec.rb +22 -15
- data/spec/unit/concerns/authentication_spec.rb +6 -6
- data/spec/unit/concerns/caching_spec.rb +26 -0
- data/spec/unit/concerns/composite_api_spec.rb +143 -0
- data/spec/unit/concerns/connection_spec.rb +2 -2
- data/spec/unit/concerns/streaming_spec.rb +4 -4
- data/spec/unit/config_spec.rb +2 -2
- data/spec/unit/error_code_spec.rb +61 -0
- data/spec/unit/mash_spec.rb +5 -0
- data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +24 -8
- data/spec/unit/middleware/authentication/password_spec.rb +12 -4
- data/spec/unit/middleware/authentication/token_spec.rb +12 -4
- data/spec/unit/middleware/authentication_spec.rb +14 -8
- data/spec/unit/middleware/gzip_spec.rb +2 -2
- data/spec/unit/middleware/raise_error_spec.rb +29 -10
- data/spec/unit/signed_request_spec.rb +1 -1
- metadata +64 -187
- data/.circleci/config.yml +0 -56
- data/lib/restforce/upload_io.rb +0 -9
data/lib/restforce/collection.rb
CHANGED
@@ -12,12 +12,12 @@ module Restforce
|
|
12
12
|
end
|
13
13
|
|
14
14
|
# Yield each value on each page.
|
15
|
-
def each
|
15
|
+
def each(&block)
|
16
16
|
@raw_page['records'].each { |record| yield Restforce::Mash.build(record, @client) }
|
17
17
|
|
18
18
|
np = next_page
|
19
19
|
while np
|
20
|
-
np.current_page.each
|
20
|
+
np.current_page.each(&block)
|
21
21
|
np = np.next_page
|
22
22
|
end
|
23
23
|
end
|
@@ -27,12 +27,35 @@ module Restforce
|
|
27
27
|
@raw_page['records'].size
|
28
28
|
end
|
29
29
|
|
30
|
-
# Return the
|
30
|
+
# Return the number of items in the Collection without making any additional
|
31
|
+
# requests and going through all of the pages of results, one by one. Instead,
|
32
|
+
# we can rely on the total count of results which Salesforce returns.
|
33
|
+
#
|
34
|
+
# Most of the Salesforce API returns this in the `totalSize` attribute. For
|
35
|
+
# some reason, the [List View Results](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_listviewresults.htm)
|
36
|
+
# endpoint (and maybe others?!) uses the `size` attribute.
|
31
37
|
def size
|
32
|
-
@raw_page['totalSize']
|
38
|
+
@raw_page['totalSize'] || @raw_page['size']
|
33
39
|
end
|
34
40
|
alias length size
|
35
41
|
|
42
|
+
def count(*args)
|
43
|
+
# By default, `Enumerable`'s `#count` uses `#each`, which means going through all
|
44
|
+
# of the pages of results, one by one. Instead, we can use `#size` which we have
|
45
|
+
# already overridden to work in a smarter, more efficient way. This only works for
|
46
|
+
# the simple version of `#count` with no arguments. When called with an argument or
|
47
|
+
# a block, you need to know what the items in the collection actually are, so we
|
48
|
+
# call `super` and end up iterating through each item in the collection.
|
49
|
+
return size unless block_given? || !args.empty?
|
50
|
+
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns true if the size of the Collection is zero.
|
55
|
+
def empty?
|
56
|
+
size.zero?
|
57
|
+
end
|
58
|
+
|
36
59
|
# Return array of the elements on the current page
|
37
60
|
def current_page
|
38
61
|
first(@raw_page['records'].size)
|
@@ -380,7 +380,7 @@ module Restforce
|
|
380
380
|
end
|
381
381
|
else
|
382
382
|
api_patch "sobjects/#{sobject}/#{field}/" \
|
383
|
-
|
383
|
+
"#{ERB::Util.url_encode(external_id)}", attrs
|
384
384
|
end
|
385
385
|
|
386
386
|
response.body.respond_to?(:fetch) ? response.body.fetch('id', true) : true
|
@@ -429,6 +429,7 @@ module Restforce
|
|
429
429
|
# field - External ID field to use (default: nil).
|
430
430
|
#
|
431
431
|
# Returns the Restforce::SObject sobject record.
|
432
|
+
# Raises NotFoundError if nothing is found.
|
432
433
|
def find(sobject, id, field = nil)
|
433
434
|
url = if field
|
434
435
|
"sobjects/#{sobject}/#{field}/#{ERB::Util.url_encode(id)}"
|
@@ -510,7 +511,7 @@ module Restforce
|
|
510
511
|
|
511
512
|
# Internal: Errors that should be rescued from in non-bang methods
|
512
513
|
def exceptions
|
513
|
-
[Faraday::
|
514
|
+
[Faraday::Error]
|
514
515
|
end
|
515
516
|
end
|
516
517
|
end
|
@@ -61,9 +61,9 @@ module Restforce
|
|
61
61
|
def initialize(opts = {})
|
62
62
|
raise ArgumentError, 'Please specify a hash of options' unless opts.is_a?(Hash)
|
63
63
|
|
64
|
-
@options =
|
64
|
+
@options = Restforce.configuration.options.to_h do |option|
|
65
65
|
[option, Restforce.configuration.send(option)]
|
66
|
-
end
|
66
|
+
end
|
67
67
|
|
68
68
|
@options.merge! opts
|
69
69
|
yield builder if block_given?
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'restforce/concerns/verbs'
|
4
|
+
|
5
|
+
module Restforce
|
6
|
+
module Concerns
|
7
|
+
module CompositeAPI
|
8
|
+
extend Restforce::Concerns::Verbs
|
9
|
+
|
10
|
+
define_verbs :post
|
11
|
+
|
12
|
+
def composite(all_or_none: false, collate_subrequests: false)
|
13
|
+
subrequests = Subrequests.new(options)
|
14
|
+
yield(subrequests)
|
15
|
+
|
16
|
+
if subrequests.requests.length > 25
|
17
|
+
raise ArgumentError, 'Cannot have more than 25 subrequests.'
|
18
|
+
end
|
19
|
+
|
20
|
+
properties = {
|
21
|
+
compositeRequest: subrequests.requests,
|
22
|
+
allOrNone: all_or_none,
|
23
|
+
collateSubrequests: collate_subrequests
|
24
|
+
}
|
25
|
+
response = api_post('composite', properties.to_json)
|
26
|
+
|
27
|
+
results = response.body['compositeResponse']
|
28
|
+
has_errors = results.any? { |result| result['httpStatusCode'].digits.last == 4 }
|
29
|
+
if all_or_none && has_errors
|
30
|
+
last_error_index = results.rindex { |result| result['httpStatusCode'] != 412 }
|
31
|
+
last_error = results[last_error_index]
|
32
|
+
raise CompositeAPIError, last_error['body'][0]['errorCode']
|
33
|
+
end
|
34
|
+
|
35
|
+
results
|
36
|
+
end
|
37
|
+
|
38
|
+
def composite!(collate_subrequests: false, &block)
|
39
|
+
composite(all_or_none: true, collate_subrequests: collate_subrequests, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
class Subrequests
|
43
|
+
def initialize(options)
|
44
|
+
@options = options
|
45
|
+
@requests = []
|
46
|
+
end
|
47
|
+
attr_reader :options, :requests
|
48
|
+
|
49
|
+
def create(sobject, reference_id, attrs)
|
50
|
+
requests << {
|
51
|
+
method: 'POST',
|
52
|
+
url: composite_api_path(sobject),
|
53
|
+
body: attrs,
|
54
|
+
referenceId: reference_id
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def update(sobject, reference_id, attrs)
|
59
|
+
id = attrs.fetch(attrs.keys.find { |k, _v| k.to_s.casecmp?('id') }, nil)
|
60
|
+
raise ArgumentError, 'Id field missing from attrs.' unless id
|
61
|
+
|
62
|
+
attrs_without_id = attrs.reject { |k, _v| k.to_s.casecmp?('id') }
|
63
|
+
requests << {
|
64
|
+
method: 'PATCH',
|
65
|
+
url: composite_api_path("#{sobject}/#{id}"),
|
66
|
+
body: attrs_without_id,
|
67
|
+
referenceId: reference_id
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def destroy(sobject, reference_id, id)
|
72
|
+
requests << {
|
73
|
+
method: 'DELETE',
|
74
|
+
url: composite_api_path("#{sobject}/#{id}"),
|
75
|
+
referenceId: reference_id
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def upsert(sobject, reference_id, ext_field, attrs)
|
80
|
+
raise ArgumentError, 'External id field missing.' unless ext_field
|
81
|
+
|
82
|
+
ext_id = attrs.fetch(attrs.keys.find do |k, _v|
|
83
|
+
k.to_s.casecmp?(ext_field.to_s)
|
84
|
+
end, nil)
|
85
|
+
raise ArgumentError, 'External id missing from attrs.' unless ext_id
|
86
|
+
|
87
|
+
attrs_without_ext_id = attrs.reject { |k, _v| k.to_s.casecmp?(ext_field) }
|
88
|
+
requests << {
|
89
|
+
method: 'PATCH',
|
90
|
+
url: composite_api_path("#{sobject}/#{ext_field}/#{ext_id}"),
|
91
|
+
body: attrs_without_ext_id,
|
92
|
+
referenceId: reference_id
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def composite_api_path(path)
|
99
|
+
"/services/data/v#{options[:api_version]}/sobjects/#{path}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -43,7 +43,7 @@ module Restforce
|
|
43
43
|
# Caches GET requests.
|
44
44
|
builder.use Restforce::Middleware::Caching, cache, options if cache
|
45
45
|
# Follows 30x redirects.
|
46
|
-
builder.use
|
46
|
+
builder.use Faraday::FollowRedirects::Middleware
|
47
47
|
# Raises errors for 40x responses.
|
48
48
|
builder.use Restforce::Middleware::RaiseError
|
49
49
|
# Parses returned JSON response into a hash.
|
@@ -35,7 +35,7 @@ module Restforce
|
|
35
35
|
@valid_for = options.delete(:valid_for)
|
36
36
|
raise "#{field} is not a dependent picklist" if @valid_for && !dependent?
|
37
37
|
|
38
|
-
|
38
|
+
super(picklist_values)
|
39
39
|
end
|
40
40
|
|
41
41
|
private
|
@@ -85,7 +85,7 @@ module Restforce
|
|
85
85
|
def valid?(picklist_entry)
|
86
86
|
valid_for = picklist_entry['validFor'].ljust(16, 'A').unpack1('m').
|
87
87
|
unpack('C*')
|
88
|
-
(valid_for[index >> 3] & (0x80 >> index % 8)).positive?
|
88
|
+
(valid_for[index >> 3] & (0x80 >> (index % 8))).positive?
|
89
89
|
end
|
90
90
|
end
|
91
91
|
end
|
@@ -95,9 +95,7 @@ module Restforce
|
|
95
95
|
|
96
96
|
def replay_id(channel)
|
97
97
|
handler = @replay_handlers[channel]
|
98
|
-
if handler.is_a?(Integer)
|
99
|
-
handler # treat it as a scalar
|
100
|
-
elsif handler.respond_to?(:[])
|
98
|
+
if handler.respond_to?(:[]) && !handler.is_a?(Integer)
|
101
99
|
# Ask for the latest replayId for this channel
|
102
100
|
handler[channel]
|
103
101
|
else
|
data/lib/restforce/config.rb
CHANGED
@@ -91,29 +91,31 @@ module Restforce
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
-
option :api_version, default: lambda { ENV
|
94
|
+
option :api_version, default: lambda { ENV.fetch('SALESFORCE_API_VERSION', '26.0') }
|
95
95
|
|
96
96
|
# The username to use during login.
|
97
|
-
option :username, default: lambda { ENV
|
97
|
+
option :username, default: lambda { ENV.fetch('SALESFORCE_USERNAME', nil) }
|
98
98
|
|
99
99
|
# The password to use during login.
|
100
|
-
option :password, default: lambda { ENV
|
100
|
+
option :password, default: lambda { ENV.fetch('SALESFORCE_PASSWORD', nil) }
|
101
101
|
|
102
102
|
# The security token to use during login.
|
103
|
-
option :security_token, default: lambda {
|
103
|
+
option :security_token, default: lambda {
|
104
|
+
ENV.fetch('SALESFORCE_SECURITY_TOKEN', nil)
|
105
|
+
}
|
104
106
|
|
105
107
|
# The OAuth client id
|
106
|
-
option :client_id, default: lambda { ENV
|
108
|
+
option :client_id, default: lambda { ENV.fetch('SALESFORCE_CLIENT_ID', nil) }
|
107
109
|
|
108
110
|
# The OAuth client secret
|
109
|
-
option :client_secret, default: lambda { ENV
|
111
|
+
option :client_secret, default: lambda { ENV.fetch('SALESFORCE_CLIENT_SECRET', nil) }
|
110
112
|
|
111
113
|
# The private key for JWT authentication
|
112
114
|
option :jwt_key
|
113
115
|
|
114
116
|
# Set this to true if you're authenticating with a Sandbox instance.
|
115
117
|
# Defaults to false.
|
116
|
-
option :host, default: lambda { ENV
|
118
|
+
option :host, default: lambda { ENV.fetch('SALESFORCE_HOST', 'login.salesforce.com') }
|
117
119
|
|
118
120
|
option :oauth_token
|
119
121
|
option :refresh_token
|
@@ -139,7 +141,7 @@ module Restforce
|
|
139
141
|
# Faraday adapter to use. Defaults to Faraday.default_adapter.
|
140
142
|
option :adapter, default: lambda { Faraday.default_adapter }
|
141
143
|
|
142
|
-
option :proxy_uri, default: lambda { ENV
|
144
|
+
option :proxy_uri, default: lambda { ENV.fetch('SALESFORCE_PROXY_URI', nil) }
|
143
145
|
|
144
146
|
# A Proc that is called with the response body after a successful authentication.
|
145
147
|
option :authentication_callback
|
@@ -151,11 +153,14 @@ module Restforce
|
|
151
153
|
option :request_headers
|
152
154
|
|
153
155
|
# Set a logger for when Restforce.log is set to true, defaulting to STDOUT
|
154
|
-
option :logger, default: ::Logger.new(
|
156
|
+
option :logger, default: ::Logger.new($stdout)
|
155
157
|
|
156
158
|
# Set a log level for logging when Restforce.log is set to true, defaulting to :debug
|
157
159
|
option :log_level, default: :debug
|
158
160
|
|
161
|
+
# Set use_cache to false to opt in to caching with client.with_caching
|
162
|
+
option :use_cache, default: true
|
163
|
+
|
159
164
|
def options
|
160
165
|
self.class.options
|
161
166
|
end
|