restforce 4.2.1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/unhandled-salesforce-error.md +17 -0
  3. data/.github/dependabot.yml +10 -0
  4. data/.github/funding.yml +1 -0
  5. data/.github/workflows/build.yml +23 -0
  6. data/.github/workflows/faraday.yml +27 -0
  7. data/.rubocop.yml +5 -4
  8. data/CHANGELOG.md +115 -0
  9. data/CONTRIBUTING.md +21 -1
  10. data/Dockerfile +31 -0
  11. data/Gemfile +15 -7
  12. data/README.md +102 -24
  13. data/UPGRADING.md +67 -0
  14. data/docker-compose.yml +7 -0
  15. data/lib/restforce/abstract_client.rb +1 -0
  16. data/lib/restforce/collection.rb +27 -4
  17. data/lib/restforce/concerns/api.rb +3 -2
  18. data/lib/restforce/concerns/base.rb +2 -2
  19. data/lib/restforce/concerns/caching.rb +7 -0
  20. data/lib/restforce/concerns/composite_api.rb +104 -0
  21. data/lib/restforce/concerns/connection.rb +1 -1
  22. data/lib/restforce/concerns/picklists.rb +2 -2
  23. data/lib/restforce/concerns/streaming.rb +1 -3
  24. data/lib/restforce/config.rb +14 -9
  25. data/lib/restforce/error_code.rb +650 -0
  26. data/lib/restforce/file_part.rb +32 -0
  27. data/lib/restforce/mash.rb +8 -3
  28. data/lib/restforce/middleware/authentication.rb +1 -0
  29. data/lib/restforce/middleware/caching.rb +140 -15
  30. data/lib/restforce/middleware/json_request.rb +90 -0
  31. data/lib/restforce/middleware/json_response.rb +85 -0
  32. data/lib/restforce/middleware/logger.rb +14 -9
  33. data/lib/restforce/middleware/raise_error.rb +13 -5
  34. data/lib/restforce/middleware.rb +2 -0
  35. data/lib/restforce/version.rb +1 -1
  36. data/lib/restforce.rb +15 -14
  37. data/restforce.gemspec +13 -21
  38. data/spec/fixtures/sobject/list_view_results_success_response.json +151 -0
  39. data/spec/integration/abstract_client_spec.rb +56 -35
  40. data/spec/integration/data/client_spec.rb +6 -2
  41. data/spec/spec_helper.rb +24 -1
  42. data/spec/support/client_integration.rb +7 -7
  43. data/spec/support/concerns.rb +1 -1
  44. data/spec/support/fixture_helpers.rb +1 -3
  45. data/spec/support/middleware.rb +1 -2
  46. data/spec/unit/collection_spec.rb +38 -2
  47. data/spec/unit/concerns/api_spec.rb +22 -15
  48. data/spec/unit/concerns/authentication_spec.rb +6 -6
  49. data/spec/unit/concerns/caching_spec.rb +26 -0
  50. data/spec/unit/concerns/composite_api_spec.rb +143 -0
  51. data/spec/unit/concerns/connection_spec.rb +2 -2
  52. data/spec/unit/concerns/streaming_spec.rb +4 -4
  53. data/spec/unit/config_spec.rb +2 -2
  54. data/spec/unit/error_code_spec.rb +61 -0
  55. data/spec/unit/mash_spec.rb +5 -0
  56. data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +24 -8
  57. data/spec/unit/middleware/authentication/password_spec.rb +12 -4
  58. data/spec/unit/middleware/authentication/token_spec.rb +12 -4
  59. data/spec/unit/middleware/authentication_spec.rb +14 -8
  60. data/spec/unit/middleware/gzip_spec.rb +2 -2
  61. data/spec/unit/middleware/raise_error_spec.rb +29 -10
  62. data/spec/unit/signed_request_spec.rb +1 -1
  63. metadata +64 -187
  64. data/.circleci/config.yml +0 -56
  65. data/lib/restforce/upload_io.rb +0 -9
@@ -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 { |record| yield record }
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 size of the Collection without making any additional requests.
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
- "#{ERB::Util.url_encode(external_id)}", attrs
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::ClientError]
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 = Hash[Restforce.configuration.options.map do |option|
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?
@@ -15,6 +15,13 @@ module Restforce
15
15
  options.delete(:use_cache)
16
16
  end
17
17
 
18
+ def with_caching
19
+ options[:use_cache] = true
20
+ yield
21
+ ensure
22
+ options[:use_cache] = false
23
+ end
24
+
18
25
  private
19
26
 
20
27
  # Internal: Cache to use for the caching middleware
@@ -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 FaradayMiddleware::FollowRedirects
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
- replace(picklist_values)
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
@@ -91,29 +91,31 @@ module Restforce
91
91
  end
92
92
  end
93
93
 
94
- option :api_version, default: lambda { ENV['SALESFORCE_API_VERSION'] || '26.0' }
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['SALESFORCE_USERNAME'] }
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['SALESFORCE_PASSWORD'] }
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 { ENV['SALESFORCE_SECURITY_TOKEN'] }
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['SALESFORCE_CLIENT_ID'] }
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['SALESFORCE_CLIENT_SECRET'] }
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['SALESFORCE_HOST'] || 'login.salesforce.com' }
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['SALESFORCE_PROXY_URI'] }
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(STDOUT)
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