restforce 5.0.6 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +4 -13
  3. data/.github/funding.yml +1 -0
  4. data/.github/workflows/build.yml +23 -0
  5. data/.github/workflows/faraday.yml +27 -0
  6. data/.rubocop.yml +2 -2
  7. data/CHANGELOG.md +68 -0
  8. data/Gemfile +15 -6
  9. data/README.md +61 -7
  10. data/UPGRADING.md +29 -0
  11. data/lib/restforce/abstract_client.rb +1 -0
  12. data/lib/restforce/collection.rb +20 -2
  13. data/lib/restforce/concerns/api.rb +2 -1
  14. data/lib/restforce/concerns/base.rb +2 -2
  15. data/lib/restforce/concerns/composite_api.rb +104 -0
  16. data/lib/restforce/concerns/connection.rb +1 -1
  17. data/lib/restforce/concerns/picklists.rb +1 -1
  18. data/lib/restforce/config.rb +12 -10
  19. data/lib/restforce/error_code.rb +30 -9
  20. data/lib/restforce/file_part.rb +12 -4
  21. data/lib/restforce/middleware/authentication.rb +1 -0
  22. data/lib/restforce/middleware/caching.rb +140 -15
  23. data/lib/restforce/middleware/gzip.rb +4 -0
  24. data/lib/restforce/middleware/json_request.rb +90 -0
  25. data/lib/restforce/middleware/json_response.rb +85 -0
  26. data/lib/restforce/middleware/logger.rb +6 -2
  27. data/lib/restforce/middleware/raise_error.rb +10 -1
  28. data/lib/restforce/version.rb +1 -1
  29. data/lib/restforce.rb +11 -7
  30. data/restforce.gemspec +8 -16
  31. data/spec/fixtures/sobject/list_view_results_success_response.json +151 -0
  32. data/spec/integration/abstract_client_spec.rb +42 -30
  33. data/spec/integration/data/client_spec.rb +6 -2
  34. data/spec/spec_helper.rb +10 -0
  35. data/spec/support/client_integration.rb +7 -7
  36. data/spec/support/concerns.rb +1 -1
  37. data/spec/support/middleware.rb +1 -2
  38. data/spec/unit/collection_spec.rb +22 -4
  39. data/spec/unit/concerns/api_spec.rb +22 -15
  40. data/spec/unit/concerns/authentication_spec.rb +6 -6
  41. data/spec/unit/concerns/base_spec.rb +1 -1
  42. data/spec/unit/concerns/composite_api_spec.rb +169 -0
  43. data/spec/unit/concerns/connection_spec.rb +1 -1
  44. data/spec/unit/concerns/streaming_spec.rb +4 -4
  45. data/spec/unit/config_spec.rb +2 -2
  46. data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +24 -8
  47. data/spec/unit/middleware/authentication/password_spec.rb +12 -4
  48. data/spec/unit/middleware/authentication/token_spec.rb +12 -4
  49. data/spec/unit/middleware/authentication_spec.rb +8 -8
  50. data/spec/unit/middleware/authorization_spec.rb +5 -1
  51. data/spec/unit/middleware/custom_headers_spec.rb +6 -2
  52. data/spec/unit/middleware/gzip_spec.rb +60 -16
  53. data/spec/unit/middleware/instance_url_spec.rb +2 -2
  54. data/spec/unit/middleware/logger_spec.rb +1 -1
  55. data/spec/unit/middleware/raise_error_spec.rb +20 -10
  56. data/spec/unit/sobject_spec.rb +9 -5
  57. metadata +55 -172
  58. data/.circleci/config.yml +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6644bf699621fb5414dce1dce275e5a265fca50305bcfca98864b6c1683f1687
4
- data.tar.gz: 5ef0177a3a91dbe1637fc493b7f0bc478fca34c49e392abb1eccdf8bf951aa66
3
+ metadata.gz: 5b88661434383f16571c2b240a9c2482035058a34adadfe39a86b4fecb7382ca
4
+ data.tar.gz: bb4ac026f9b7fbd43f7b5237760e341d9d351c5433ca19009daa24794be251f0
5
5
  SHA512:
6
- metadata.gz: 7c7d3e8b58743f8d4ae6ae9255c2d89cc78b06a5e963350784ff3c884396bf1ef03fb986f2aba7bd231d8ae9af2f0b2af78a77c98fcb62356ac09b1411753e13
7
- data.tar.gz: 5c8bb85fdcd28e5eba8d0939ef1868d42b99d079cddd056941ec7c4cf591d3789caa2b569cafeb8415a89755e567de1a93486dc9c18c5507275adad5631374cc
6
+ metadata.gz: da66afd00a69c078e3883f76ae0039d2742911de7b0ca8cb6b14b15801695973a25deec7e71fede838d9193a60224510eee625b72b06c827d6f0467111757023
7
+ data.tar.gz: 86cad2e9fecc5610dae221bdec91a8a1868b9333d7aa552b5fb21af928352c73cad50ea64f63f26cd61ea50048784f5c3dad20b04d798c70aee2f0295c1721bd
@@ -4,16 +4,7 @@ updates:
4
4
  directory: "/"
5
5
  schedule:
6
6
  interval: daily
7
- open-pull-requests-limit: 10
8
- ignore:
9
- - dependency-name: rubocop
10
- versions:
11
- - 1.10.0
12
- - 1.11.0
13
- - 1.12.0
14
- - 1.12.1
15
- - 1.9.0
16
- - dependency-name: webmock
17
- versions:
18
- - 3.12.0
19
- - 3.12.1
7
+ - package-ecosystem: 'github-actions'
8
+ directory: '/'
9
+ schedule:
10
+ interval: 'daily'
@@ -0,0 +1 @@
1
+ github: [restforce]
@@ -0,0 +1,23 @@
1
+ name: 'Build'
2
+ on: push
3
+ jobs:
4
+ lint-and-test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ ruby_version: ['2.7', '3.0', '3.1', '3.2']
10
+ steps:
11
+ - name: Checkout code
12
+ uses: actions/checkout@v3
13
+ - name: Setup Ruby ${{ matrix.ruby_version }}
14
+ uses: ruby/setup-ruby@v1
15
+ with:
16
+ bundler-cache: true
17
+ ruby-version: ${{ matrix.ruby_version }}
18
+ - name: Install dependencies
19
+ run: bundle install
20
+ - name: Lint Ruby files
21
+ run: bundle exec rubocop
22
+ - name: Run RSpec tests
23
+ run: bundle exec rspec
@@ -0,0 +1,27 @@
1
+ name: 'Test against supported Faraday minor versions'
2
+ on: push
3
+ jobs:
4
+ lint-and-test:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ # Normally, we only test the most recent patch version for any given minor version.
10
+ # For v2.0.x, we test v2.0.0 and v2.0.1 because v2.0.0 has a special behaviour where
11
+ # the Net::HTTP adapter is not included. See
12
+ # https://github.com/lostisland/faraday/blob/main/UPGRADING.md#faraday-20.
13
+ faraday_version: ['1.1.0', '1.2.0', '1.3.1', '1.4.1', '1.5.1', '1.6.0', '1.7.2', '1.8.0', '1.9.3', '1.10.0', '2.0.0', '2.0.1', '2.1.0', '2.2.0', '2.3.0', '2.4.0', '2.5.0', '2.6.0', '2.7.0']
14
+ env:
15
+ FARADAY_VERSION: ~> ${{ matrix.faraday_version }}
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v3
19
+ - name: Setup Ruby ${{ matrix.ruby_version }}
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ bundler-cache: true
23
+ ruby-version: 3.1
24
+ - name: Install dependencies
25
+ run: bundle install
26
+ - name: Run RSpec tests
27
+ run: bundle exec rspec
data/.rubocop.yml CHANGED
@@ -8,7 +8,7 @@ AllCops:
8
8
  - .*/**/*
9
9
  - vendor/**/*
10
10
  NewCops: enable
11
- TargetRubyVersion: 2.5
11
+ TargetRubyVersion: 2.7
12
12
 
13
13
  # Limit lines to 90 characters.
14
14
  Layout/LineLength:
@@ -71,4 +71,4 @@ Naming/FileName:
71
71
  - Guardfile
72
72
 
73
73
  Lint/UriEscapeUnescape:
74
- Enabled: false
74
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,71 @@
1
+ # 6.2.1 (Jan 18 2023)
2
+
3
+ * When a response claims to be gzipped but isn't, return the body as it is and don't explode (@timrogers)
4
+
5
+ # 6.2.0 (Jan 18 2023)
6
+
7
+ * Add support for `faraday` v2.7.x (@timrogers)
8
+ * Expose the Faraday `Response` on `CompositeAPIError`s (@shravan097)
9
+
10
+ # 6.1.0 (Nov 18 2022)
11
+
12
+ * Add support for `faraday` v2.5.x and v2.6.x (@marcrohloff, @timrogers)
13
+
14
+ # 6.0.0 (Sep 9 2022)
15
+
16
+ __This version contains breaking changes. For help with upgrading, see [`UPGRADING.md`](https://github.com/restforce/restforce/blob/main/UPGRADING.md).__
17
+
18
+ * __⚠️ Drop support for Ruby 2.6__, since [Ruby 2.6 has reached its end-of-life](https://www.ruby-lang.org/en/downloads/) (@timrogers)
19
+ * __⚠️ Drop compatability with `faraday` versions before `v1.1.0`__ (@timrogers)
20
+ * Add support for `faraday` versions `v2.0.0` onwards (@timrogers)
21
+
22
+ *This version was also released as [`v6.0.0.rc.1`](https://github.com/restforce/restforce/releases/tag/v6.0.0.rc.1) on Aug 4 2022.*
23
+
24
+ # 5.3.1 (Jul 19 2022)
25
+
26
+ * Handle the `EXCEEDED_ID_LIMIT` error returned by the Salesforce API (@timrogers, @yashshah1)
27
+
28
+ # 5.3.0 (May 30, 2022)
29
+
30
+ * Add support for Faraday v1.9.x and v1.10.0 (@magni-, @timrogers)
31
+ * Follow redirects during authentication to support Lightning URLs (e.g. `*.lightning.force.com` instead of `*.my.salesforce.com`) (@nhocki)
32
+
33
+ # 5.2.4 (Mar 16, 2022)
34
+
35
+ * Fix `Restforce::Collection#size` for Salesforce APIs that use the `size` property to return the total number of results, instead of `totalSize` (@kwong-yw)
36
+
37
+ # 5.2.3 (Jan 17, 2022)
38
+
39
+ * Add official support for Ruby 3.1 (@timrogers)
40
+ * Fix handling of responses from the Composite API (@robob27)
41
+ * Fix dependencies to correctly declare that the gem doesn't work with [faraday](https://github.com/lostisland/faraday) `v1.9.0` or later (@timrogers)
42
+
43
+ # 5.2.2 (Dec 16, 2021)
44
+
45
+ * Handle the `MALFORMED_SEARCH` error returned by Salesforce (@timrogers)
46
+
47
+ # 5.2.1 (Dec 8, 2021)
48
+
49
+ * Handle the `OPERATION_TOO_LARGE` error returned by Salesforce (@timrogers)
50
+ * Handle the `INVALID_SIGNUP_COUNTRY` error returned by Salesforce (@timrogers)
51
+
52
+ ## 5.2.0 (Oct 15, 2021)
53
+
54
+ * Add support for Salesforce's Composite API and Composite Batch API (@meenie, @amacdougall)
55
+ * Improve the performance of counting numbers of query results with `Restforce::Collection#count`, avoiding unnecessary API requests (@jhass)
56
+
57
+ ## 5.1.1 (Oct 13, 2021)
58
+
59
+ * Handle the `INVALID_REPLICATION_DATE` error returned by Salesforce (@michaelwnyc)
60
+ * Handle the `BIG_OBJECT_UNSUPPORTED_OPERATION` error returned by Salesforce (@remon)
61
+
62
+ ## 5.1.0 (Aug 26, 2021)
63
+
64
+ * Add official support for Ruby 3.0 (@timrogers)
65
+ * Drop support for Ruby 2.5, which has reached end-of-life (@timrogers)
66
+ * Handle the `QUERY_TIMEOUT` error returned by Salesforce (@timrogers)
67
+ * Remove unnecessary development dependencies for the gem, which can just be in the project's `Gemfile` (@timrogers)
68
+
1
69
  ## 5.0.6 (Jun 17, 2021)
2
70
 
3
71
  * Handle the `API_DISABLED_FOR_ORG` error returned by Salesforce (@cmac)
data/Gemfile CHANGED
@@ -3,11 +3,20 @@
3
3
  source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
+ faraday_version = ENV.fetch('FARADAY_VERSION', '~> 2.7.3')
7
+
8
+ # Enable us to explicitly pick a Faraday version when running tests
9
+ gem 'faraday', faraday_version
10
+ gem 'faraday-typhoeus', '~> 0.2.1' unless faraday_version.start_with?("~> 1")
11
+ gem 'faye' unless RUBY_PLATFORM == 'java'
12
+ gem 'guard-rspec'
13
+ gem 'guard-rubocop'
6
14
  gem 'jruby-openssl', platforms: :jruby
7
- gem 'jwt'
8
15
  gem 'rake'
9
-
10
- group :development do
11
- gem 'guard-rspec'
12
- gem 'guard-rubocop'
13
- end
16
+ gem 'rspec', '~> 3.12.0'
17
+ gem 'rspec-collection_matchers', '~> 1.2.0'
18
+ gem 'rspec-its', '~> 1.3.0'
19
+ gem 'rspec_junit_formatter', '~> 0.6.0'
20
+ gem 'rubocop', '~> 1.43.0'
21
+ gem 'simplecov', '~> 0.22.0'
22
+ gem 'webmock', '~> 3.18.1'
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Restforce
2
2
 
3
- [![CircleCI](https://circleci.com/gh/restforce/restforce.svg?style=svg)](https://circleci.com/gh/restforce/restforce) [![Code Climate](https://codeclimate.com/github/restforce/restforce.png)](https://codeclimate.com/github/restforce/restforce) [![Dependency Status](https://gemnasium.com/restforce/restforce.png)](https://gemnasium.com/restforce/restforce)
4
- ![](https://img.shields.io/gem/dt/restforce.svg)
3
+ [![CircleCI](https://circleci.com/gh/restforce/restforce.svg?style=svg)](https://circleci.com/gh/restforce/restforce)
4
+ ![Downloads](https://img.shields.io/gem/dt/restforce.svg)
5
5
 
6
6
  Restforce is a ruby gem for the [Salesforce REST api](http://www.salesforce.com/us/developer/docs/api_rest/index.htm).
7
7
 
@@ -12,6 +12,8 @@ Features include:
12
12
  * Support for parent-to-child relationships.
13
13
  * Support for aggregate queries.
14
14
  * Support for the [Streaming API](#streaming)
15
+ * Support for the [Composite API](#composite-api)
16
+ * Support for the [Composite Batch API](#composite-batch-api)
15
17
  * Support for the GetUpdated API
16
18
  * Support for blob data types.
17
19
  * Support for GZIP compression.
@@ -25,7 +27,7 @@ Features include:
25
27
 
26
28
  Add this line to your application's Gemfile:
27
29
 
28
- gem 'restforce', '~> 5.0.6'
30
+ gem 'restforce', '~> 6.2.1'
29
31
 
30
32
  And then execute:
31
33
 
@@ -35,8 +37,10 @@ Or install it yourself as:
35
37
 
36
38
  $ gem install restforce
37
39
 
38
- __As of version 5.0.0, this gem is only compatible with Ruby 2.5.0 and later.__ If you're using an earlier Ruby version:
40
+ __As of version 6.0.0, this gem is only compatible with Ruby 2.7.0 and later.__ If you're using an earlier Ruby version:
39
41
 
42
+ * for Ruby 2.6, use version 5.3.1 or earlier
43
+ * for Ruby 2.5, use version 5.0.6 or earlier
40
44
  * for Ruby 2.4, use version 4.3.0 or earlier
41
45
  * for Ruby 2.3, use version 3.2.0 or earlier
42
46
  * for Ruby versions 2.2, 2.1 and 2.0, use version 2.5.3 or earlier
@@ -147,6 +151,8 @@ export SALESFORCE_API_VERSION="41.0"
147
151
  client = Restforce.new
148
152
  ```
149
153
 
154
+ **Note:** Restforce library does not cache JWT Bearer tokens automatically. This means that every instantiation of the Restforce class will be treated as a new login by Salesforce. Remember that Salesforce enforces [rate limits on login requests](https://help.salesforce.com/s/articleView?id=000312767&type=1). If you are building an application that will instantiate the Restforce class more than this specified rate limit, you might want to consider caching the Bearer token either in-memory or in your own storage by leveraging the `authentication_callback` method.
155
+
150
156
  #### Sandbox Organizations
151
157
 
152
158
  You can connect to sandbox organizations by specifying a host. The default host is
@@ -188,7 +194,7 @@ end
188
194
 
189
195
  By default, the gem defaults to using Version 26.0 (Winter '13) of the Salesforce API. This maintains backwards compatibility for existing users.
190
196
 
191
- __We strongly suggest configuring Restforce to use the most recent API version, currently Version 41.0 (Winter '18) to get the best Salesforce API experience__ - for example, some more recently-added API endpoints will not be available without moving to a more recent
197
+ __We strongly suggest configuring Restforce to use the [most recent API version](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_versions.htm), to get the best Salesforce API experience__ - for example, some more recently-added API endpoints will not be available without moving to a more recent
192
198
  version. If you're trying to use a method that is unavailable with your API version,
193
199
  Restforce will raise an `APIVersionError`.
194
200
 
@@ -290,6 +296,8 @@ client.find('Account', '1234', 'Some_External_Id_Field__c')
290
296
  # => #<Restforce::SObject Id="001D000000INjVe" Name="Test" LastModifiedBy="005G0000002f8FHIAY" ... >
291
297
  ```
292
298
 
299
+ `find` raises an error if nothing is found.
300
+
293
301
  ### select
294
302
 
295
303
  `select` allows the fetching of a specific list of fields from a single object. It requires an `external_id` lookup, but is often much faster than an arbitrary query.
@@ -459,7 +467,7 @@ info.user_id
459
467
 
460
468
  ### File Uploads
461
469
 
462
- Using the new [Blob Data](http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_insert_update_blob.htm) api feature (500mb limit):
470
+ Using the new [Blob Data](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_insert_update_blob.htm) api feature (500mb limit):
463
471
 
464
472
  ```ruby
465
473
  client.create('Document', FolderId: '00lE0000000FJ6H',
@@ -477,7 +485,7 @@ client.create('Document', FolderId: '00lE0000000FJ6H',
477
485
  Body: Base64::encode64(File.read('image.jpg'))
478
486
  ```
479
487
 
480
- _See also: [Inserting or updating blob data](http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_insert_update_blob.htm)_
488
+ _See also: [Inserting or updating blob data](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_insert_update_blob.htm)_
481
489
 
482
490
  * * *
483
491
 
@@ -572,6 +580,52 @@ end
572
580
  Boom, you're now receiving push notifications when Accounts are
573
581
  created/updated.
574
582
 
583
+ #### Composite API
584
+
585
+ Restforce supports the [Composite API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_composite.htm).
586
+ This feature permits the user to send a composite object—that is, a complex
587
+ object with nested children—in a single API call. Up to 25 requests may be
588
+ included in a single composite.
589
+
590
+ Note that `GET` is not yet implemented for this API.
591
+
592
+ ```ruby
593
+ # build up an array of requests:
594
+ requests << {
595
+ method: :update,
596
+ sobject: sobject, # e.g. "Contact"
597
+ reference_id: reference_id,
598
+ data: data
599
+ }
600
+
601
+ # send every 25 requests as a subrequest in a single composite call
602
+ requests.each_slice(25).map do |req_slice|
603
+ client.composite do |subrequest|
604
+ req_slice.each do |r|
605
+ subrequest.send *r.values
606
+ end
607
+ end
608
+ end
609
+
610
+ # note that we're using `map` to return an array of each responses to each
611
+ # composite call; 100 requests will produce 4 responses
612
+ ```
613
+
614
+ #### Composite Batch API
615
+
616
+ Restforce supports the [Composite Batch API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_batch.htm).
617
+ This feature permits up to 25 subrequests in a single request, though each
618
+ subrequest counts against the API limit. On the other hand, it has fewer
619
+ limitations than the Composite API.
620
+
621
+ ```
622
+ client.batch do |subrequests|
623
+ subrequests.create('Object', name: 'test')
624
+ subrequests.update('Object', id: '123', name: 'test')
625
+ subrequests.destroy('Object', '123')
626
+ end
627
+ ```
628
+
575
629
  #### Replaying Events
576
630
 
577
631
  Since API version 37.0, Salesforce stores events for 24 hours and they can be
data/UPGRADING.md CHANGED
@@ -1,3 +1,32 @@
1
+ # Upgrading from Restforce 5.x to 6.x
2
+
3
+ __There are two breaking changes introduced in Restforce 6.x__. In this guide, you'll learn about these changes and what you should check in your code to make sure that it will work with the latest version of the library.
4
+
5
+ ## Versions of `faraday` before `v1.1.0` are no longer supported
6
+
7
+ __Likelyhood of impact__: Moderate
8
+
9
+ Restforce uses a gem called [`faraday`](https://github.com/lostisland/faraday) to make HTTP requests to Salesforce.
10
+
11
+ Up until now, Restforce has supported Faraday versions between v0.9.0 and v1.10.0.
12
+
13
+ In Restforce 6.x, we drop support for Faraday versions before v1.1.0, and add support for Faraday v2.x.
14
+
15
+ This will allow you to use the latest versions of Faraday and benefit from security patches, new features, etc., but you may need to adapt your code. The impact of this change will depend on your project:
16
+
17
+ * If Restforce is the only part of your project using Faraday - that is, your own code doesn't use Faraday and none of your other gems use Faraday - then you shouldn't need to do anything special. Just upgrade Restforce, and everything should be handled automatically.
18
+ * If your own code uses Faraday or another gem you use depends on Faraday, and you're currently using a Faraday version before v1.1.0, you will need to upgrade your Faraday version. If possible, you should upgrade to the latest version (v2.4.0 at the time of writing). This may require you to adapt your code (see [here](https://github.com/lostisland/faraday/blob/main/UPGRADING.md) for Faraday's instructions) or upgrade other gems you depend on.
19
+
20
+ ## Ruby 2.6 is no longer supported
21
+
22
+ __Likelyhood of impact__: Moderate
23
+
24
+ Ruby 2.6 is no longer officially supported as an active version of the Ruby language. That means that it will not receive patches and security fixes.
25
+
26
+ Accordingly, we've dropped support for Ruby 2.6 and earlier in the Restforce library. The gemspec now specifies that only 2.7 onwards is supported, and this will be enforced by RubyGems.
27
+
28
+ Before you update to Restforce 6.x, you'll need to switch to Ruby 2.7 or later. The current version of Ruby at the time of wriing is 3.1.
29
+
1
30
  # Upgrading from Restforce 4.x to 5.x
2
31
 
3
32
  __There are three breaking changes introduced in Restforce 5.x__. In this guide, you'll learn about these changes and what you should check in your code to make sure that it will work with the latest version of the library.
@@ -8,5 +8,6 @@ module Restforce
8
8
  include Restforce::Concerns::Caching
9
9
  include Restforce::Concerns::API
10
10
  include Restforce::Concerns::BatchAPI
11
+ include Restforce::Concerns::CompositeAPI
11
12
  end
12
13
  end
@@ -27,12 +27,30 @@ 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
+
36
54
  # Returns true if the size of the Collection is zero.
37
55
  def empty?
38
56
  size.zero?
@@ -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)}"
@@ -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 = Restforce.configuration.options.map do |option|
64
+ @options = Restforce.configuration.options.to_h do |option|
65
65
  [option, Restforce.configuration.send(option)]
66
- end.to_h
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.new(last_error['body'][0]['errorCode'], response)
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.
@@ -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
@@ -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
- # Set this to true if you're authenticating with a Sandbox instance.
115
- # Defaults to false.
116
- option :host, default: lambda { ENV['SALESFORCE_HOST'] || 'login.salesforce.com' }
116
+ # The login host.
117
+ # Defaults to 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