restforce 3.0.1 → 5.1.1
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 +5 -5
- data/.circleci/config.yml +9 -9
- data/.github/ISSUE_TEMPLATE/unhandled-salesforce-error.md +17 -0
- data/.github/dependabot.yml +19 -0
- data/.rubocop.yml +13 -14
- data/.rubocop_todo.yml +128 -81
- data/CHANGELOG.md +107 -1
- data/CONTRIBUTING.md +21 -1
- data/Dockerfile +31 -0
- data/Gemfile +10 -6
- data/README.md +168 -31
- data/UPGRADING.md +38 -0
- data/docker-compose.yml +7 -0
- data/lib/restforce/abstract_client.rb +1 -0
- data/lib/restforce/attachment.rb +1 -0
- data/lib/restforce/collection.rb +7 -2
- data/lib/restforce/concerns/api.rb +10 -7
- data/lib/restforce/concerns/authentication.rb +10 -0
- data/lib/restforce/concerns/base.rb +4 -2
- data/lib/restforce/concerns/batch_api.rb +87 -0
- data/lib/restforce/concerns/caching.rb +7 -0
- data/lib/restforce/concerns/canvas.rb +1 -0
- data/lib/restforce/concerns/connection.rb +3 -3
- data/lib/restforce/concerns/picklists.rb +4 -3
- data/lib/restforce/concerns/streaming.rb +73 -3
- data/lib/restforce/config.rb +8 -1
- data/lib/restforce/document.rb +1 -0
- data/lib/restforce/error_code.rb +638 -0
- data/lib/restforce/file_part.rb +24 -0
- data/lib/restforce/mash.rb +8 -3
- data/lib/restforce/middleware/authentication/jwt_bearer.rb +38 -0
- data/lib/restforce/middleware/authentication.rb +7 -3
- data/lib/restforce/middleware/caching.rb +1 -1
- data/lib/restforce/middleware/instance_url.rb +1 -1
- data/lib/restforce/middleware/logger.rb +8 -7
- data/lib/restforce/middleware/multipart.rb +1 -0
- data/lib/restforce/middleware/raise_error.rb +24 -9
- data/lib/restforce/middleware.rb +2 -0
- data/lib/restforce/signed_request.rb +1 -0
- data/lib/restforce/sobject.rb +1 -0
- data/lib/restforce/tooling/client.rb +3 -3
- data/lib/restforce/version.rb +1 -1
- data/lib/restforce.rb +21 -3
- data/restforce.gemspec +11 -20
- data/spec/fixtures/test_private.key +27 -0
- data/spec/integration/abstract_client_spec.rb +83 -33
- 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 +3 -5
- data/spec/support/middleware.rb +1 -2
- data/spec/unit/collection_spec.rb +20 -2
- data/spec/unit/concerns/api_spec.rb +12 -12
- data/spec/unit/concerns/authentication_spec.rb +39 -4
- data/spec/unit/concerns/batch_api_spec.rb +107 -0
- data/spec/unit/concerns/caching_spec.rb +26 -0
- data/spec/unit/concerns/connection_spec.rb +2 -2
- data/spec/unit/concerns/streaming_spec.rb +144 -4
- data/spec/unit/config_spec.rb +1 -1
- 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 +62 -0
- data/spec/unit/middleware/authentication/password_spec.rb +2 -2
- data/spec/unit/middleware/authentication/token_spec.rb +2 -2
- data/spec/unit/middleware/authentication_spec.rb +31 -4
- data/spec/unit/middleware/gzip_spec.rb +2 -2
- data/spec/unit/middleware/raise_error_spec.rb +57 -17
- data/spec/unit/signed_request_spec.rb +1 -1
- data/spec/unit/sobject_spec.rb +2 -5
- metadata +39 -108
- data/lib/restforce/upload_io.rb +0 -9
data/README.md
CHANGED
@@ -8,7 +8,7 @@ Restforce is a ruby gem for the [Salesforce REST api](http://www.salesforce.com/
|
|
8
8
|
Features include:
|
9
9
|
|
10
10
|
* A clean and modular architecture using [Faraday middleware](https://github.com/technoweenie/faraday) and [Hashie::Mash](https://github.com/intridea/hashie/tree/v1.2.0)'d responses.
|
11
|
-
* Support for interacting with multiple users from different
|
11
|
+
* Support for interacting with multiple users from different organizations.
|
12
12
|
* Support for parent-to-child relationships.
|
13
13
|
* Support for aggregate queries.
|
14
14
|
* Support for the [Streaming API](#streaming)
|
@@ -19,13 +19,13 @@ Features include:
|
|
19
19
|
* Support for dependent picklists.
|
20
20
|
* Support for decoding [Force.com Canvas](http://www.salesforce.com/us/developer/docs/platform_connectpre/canvas_framework.pdf) signed requests. (NEW!)
|
21
21
|
|
22
|
-
[Official Website](
|
22
|
+
[Official Website](https://restforce.github.io/) | [Documentation](http://rubydoc.info/gems/restforce/frames) | [Changelog](https://github.com/restforce/restforce/tree/master/CHANGELOG.md)
|
23
23
|
|
24
24
|
## Installation
|
25
25
|
|
26
26
|
Add this line to your application's Gemfile:
|
27
27
|
|
28
|
-
gem 'restforce', '~>
|
28
|
+
gem 'restforce', '~> 5.1.1'
|
29
29
|
|
30
30
|
And then execute:
|
31
31
|
|
@@ -35,7 +35,13 @@ Or install it yourself as:
|
|
35
35
|
|
36
36
|
$ gem install restforce
|
37
37
|
|
38
|
-
__As of
|
38
|
+
__As of version 5.1.0, this gem is only compatible with Ruby 2.6.0 and later.__ If you're using an earlier Ruby version:
|
39
|
+
|
40
|
+
* for Ruby 2.5, use version 5.0.6 or earlier
|
41
|
+
* for Ruby 2.4, use version 4.3.0 or earlier
|
42
|
+
* for Ruby 2.3, use version 3.2.0 or earlier
|
43
|
+
* for Ruby versions 2.2, 2.1 and 2.0, use version 2.5.3 or earlier
|
44
|
+
* for Ruby 1.9.3, use version 2.4.2
|
39
45
|
|
40
46
|
This gem is versioned using [Semantic Versioning](http://semver.org/), so you can be confident when updating that there will not be breaking changes outside of a major version (following format MAJOR.MINOR.PATCH, so for instance moving from 3.1.0 to 4.0.0 would be allowed to include incompatible API changes). See the [changelog](https://github.com/restforce/restforce/tree/master/CHANGELOG.md) for details on what has changed in each version.
|
41
47
|
|
@@ -43,12 +49,12 @@ This gem is versioned using [Semantic Versioning](http://semver.org/), so you ca
|
|
43
49
|
|
44
50
|
Restforce is designed with flexibility and ease of use in mind. By default, all API calls will
|
45
51
|
return [Hashie::Mash](https://github.com/intridea/hashie/tree/v1.2.0) objects,
|
46
|
-
so you can do things like `client.query('select Id, (select Name from Children__r) from Account').Children__r.first.Name`.
|
52
|
+
so you can do things like `client.query('select Id, (select Name from Children__r) from Account').first.Children__r.first.Name`.
|
47
53
|
|
48
54
|
### Initialization
|
49
55
|
|
50
56
|
Which authentication method you use really depends on your use case. If you're
|
51
|
-
building an application where many users from different
|
57
|
+
building an application where many users from different organizations are authenticated
|
52
58
|
through oauth and you need to interact with data in their org on their behalf,
|
53
59
|
you should use the OAuth token authentication method.
|
54
60
|
|
@@ -78,7 +84,7 @@ client = Restforce.new(oauth_token: 'access_token',
|
|
78
84
|
api_version: '41.0')
|
79
85
|
```
|
80
86
|
|
81
|
-
The middleware will use the `refresh_token` automatically to acquire a new `access_token` if the existing `access_token` is invalid.
|
87
|
+
The middleware will use the `refresh_token` automatically to acquire a new `access_token` if the existing `access_token` is invalid. The refresh process uses the `host` option so make sure that is set correctly for sandbox organizations.
|
82
88
|
|
83
89
|
`authentication_callback` is a proc that handles the response from Salesforce when the `refresh_token` is used to obtain a new `access_token`. This allows the `access_token` to be saved for re-use later - otherwise subsequent API calls will continue the cycle of "auth failure/issue new access_token/auth success".
|
84
90
|
|
@@ -111,6 +117,21 @@ client = Restforce.new(username: 'foo',
|
|
111
117
|
api_version: '41.0')
|
112
118
|
```
|
113
119
|
|
120
|
+
#### JWT Bearer Token
|
121
|
+
|
122
|
+
If you prefer to use a [JWT Bearer Token](https://developer.salesforce.com/page/Digging_Deeper_into_OAuth_2.0_on_Force.com#Obtaining_an_Access_Token_using_a_JWT_Bearer_Token) to authenticate:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
client = Restforce.new(username: 'foo',
|
126
|
+
client_id: 'client_id',
|
127
|
+
instance_url: 'instance_url',
|
128
|
+
jwt_key: 'certificate_private_key',
|
129
|
+
api_version: '38.0')
|
130
|
+
```
|
131
|
+
|
132
|
+
The `jwt_key` option is the private key of the certificate uploaded to your Connected App in Salesforce.
|
133
|
+
Choose "use digital signatures" in the Connected App configuration screen to upload your certificate.
|
134
|
+
|
114
135
|
You can also set the username, password, security token, client ID, client
|
115
136
|
secret and API version in environment variables:
|
116
137
|
|
@@ -127,7 +148,17 @@ export SALESFORCE_API_VERSION="41.0"
|
|
127
148
|
client = Restforce.new
|
128
149
|
```
|
129
150
|
|
130
|
-
|
151
|
+
#### Sandbox Organizations
|
152
|
+
|
153
|
+
You can connect to sandbox organizations by specifying a host. The default host is
|
154
|
+
'login.salesforce.com':
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
client = Restforce.new(host: 'test.salesforce.com')
|
158
|
+
```
|
159
|
+
The host can also be set with the environment variable `SALESFORCE_HOST`.
|
160
|
+
|
161
|
+
#### Proxy Support
|
131
162
|
|
132
163
|
You can specify a HTTP proxy using the `proxy_uri` option, as follows, or by setting the `SALESFORCE_PROXY_URI` environment variable:
|
133
164
|
|
@@ -143,16 +174,6 @@ client = Restforce.new(username: 'foo',
|
|
143
174
|
|
144
175
|
You may specify a username and password for the proxy with a URL along the lines of 'http://user:password@proxy.example.com:123'.
|
145
176
|
|
146
|
-
#### Sandbox Orgs
|
147
|
-
|
148
|
-
You can connect to sandbox orgs by specifying a host. The default host is
|
149
|
-
'login.salesforce.com':
|
150
|
-
|
151
|
-
```ruby
|
152
|
-
client = Restforce.new(host: 'test.salesforce.com')
|
153
|
-
```
|
154
|
-
The host can also be set with the environment variable `SALESFORCE_HOST`.
|
155
|
-
|
156
177
|
#### Global configuration
|
157
178
|
|
158
179
|
You can set any of the options passed into `Restforce.new` globally:
|
@@ -313,8 +334,11 @@ client.update('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
|
|
313
334
|
```ruby
|
314
335
|
# Update the record with external `External__c` external ID set to '12'
|
315
336
|
client.upsert('Account', 'External__c', External__c: 12, Name: 'Foobar')
|
337
|
+
# => true or "RecordId"
|
316
338
|
```
|
317
339
|
|
340
|
+
The upsert method will return the record Id if included in the response body from the Salesforce API; otherwise, it returns true. Currently the Salesforce API only returns the Id for newly created records.
|
341
|
+
|
318
342
|
### destroy
|
319
343
|
|
320
344
|
```ruby
|
@@ -436,13 +460,13 @@ info.user_id
|
|
436
460
|
|
437
461
|
### File Uploads
|
438
462
|
|
439
|
-
Using the new [Blob Data](
|
463
|
+
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):
|
440
464
|
|
441
465
|
```ruby
|
442
466
|
client.create('Document', FolderId: '00lE0000000FJ6H',
|
443
467
|
Description: 'Document test',
|
444
468
|
Name: 'My image',
|
445
|
-
Body: Restforce::
|
469
|
+
Body: Restforce::FilePart.new(File.expand_path('image.jpg', __FILE__), 'image/jpeg')
|
446
470
|
```
|
447
471
|
|
448
472
|
Using base64 encoded data (37.5mb limit):
|
@@ -454,7 +478,7 @@ client.create('Document', FolderId: '00lE0000000FJ6H',
|
|
454
478
|
Body: Base64::encode64(File.read('image.jpg'))
|
455
479
|
```
|
456
480
|
|
457
|
-
_See also: [Inserting or updating blob data](
|
481
|
+
_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)_
|
458
482
|
|
459
483
|
* * *
|
460
484
|
|
@@ -473,11 +497,11 @@ document = client.query('select Id, Name, Body from Document').first
|
|
473
497
|
File.open(document.Name, 'wb') { |f| f.write(document.Body) }
|
474
498
|
```
|
475
499
|
|
476
|
-
**Note:** The example above is only applicable if your SOQL query returns a single Document record. If more than one record is returned,
|
500
|
+
**Note:** The example above is only applicable if your SOQL query returns a single Document record. If more than one record is returned,
|
477
501
|
the Body field contains an URL to retrieve the BLOB content for the first 2000 records returned. Subsequent records contain the BLOB content
|
478
|
-
in the Body field. This is confusing and hard to debug. See notes in [Issue #301](https://github.com/restforce/restforce/issues/301#issuecomment-298972959) explaining this detail.
|
479
|
-
**Executive Summary:** Don't retrieve the Body field in a SOQL query; instead, use the BLOB retrieval URL documented
|
480
|
-
in [SObject BLOB Retrieve](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_blob_retrieve.htm)
|
502
|
+
in the Body field. This is confusing and hard to debug. See notes in [Issue #301](https://github.com/restforce/restforce/issues/301#issuecomment-298972959) explaining this detail.
|
503
|
+
**Executive Summary:** Don't retrieve the Body field in a SOQL query; instead, use the BLOB retrieval URL documented
|
504
|
+
in [SObject BLOB Retrieve](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_blob_retrieve.htm)
|
481
505
|
|
482
506
|
* * *
|
483
507
|
|
@@ -513,8 +537,10 @@ client.get('/services/apexrest/FieldCase', company: 'GenePoint')
|
|
513
537
|
|
514
538
|
### Streaming
|
515
539
|
|
516
|
-
Restforce supports the [Streaming API](
|
517
|
-
pub/sub with Salesforce a trivial task
|
540
|
+
Restforce supports the [Streaming API](https://trailhead.salesforce.com/en/content/learn/modules/api_basics/api_basics_streaming), and makes implementing
|
541
|
+
pub/sub with Salesforce a trivial task.
|
542
|
+
|
543
|
+
Here is an example of creating and subscribing to a `PushTopic`:
|
518
544
|
|
519
545
|
```ruby
|
520
546
|
# Restforce uses faye as the underlying implementation for CometD.
|
@@ -523,7 +549,7 @@ require 'faye'
|
|
523
549
|
# Initialize a client with your username/password/oauth token/etc.
|
524
550
|
client = Restforce.new(username: 'foo',
|
525
551
|
password: 'bar',
|
526
|
-
security_token: 'security token'
|
552
|
+
security_token: 'security token',
|
527
553
|
client_id: 'client_id',
|
528
554
|
client_secret: 'client_secret')
|
529
555
|
|
@@ -538,7 +564,7 @@ client.create!('PushTopic',
|
|
538
564
|
|
539
565
|
EM.run do
|
540
566
|
# Subscribe to the PushTopic.
|
541
|
-
client.
|
567
|
+
client.subscription '/topic/AllAccounts' do |message|
|
542
568
|
puts message.inspect
|
543
569
|
end
|
544
570
|
end
|
@@ -547,7 +573,102 @@ end
|
|
547
573
|
Boom, you're now receiving push notifications when Accounts are
|
548
574
|
created/updated.
|
549
575
|
|
550
|
-
|
576
|
+
#### Replaying Events
|
577
|
+
|
578
|
+
Since API version 37.0, Salesforce stores events for 24 hours and they can be
|
579
|
+
replayed if your application experienced some downtime.
|
580
|
+
|
581
|
+
In order to replay past events, all you need to do is specify the last known
|
582
|
+
event ID when subscribing and you will receive all events that happened since
|
583
|
+
that event ID:
|
584
|
+
|
585
|
+
```ruby
|
586
|
+
EM.run {
|
587
|
+
# Subscribe to the PushTopic.
|
588
|
+
client.subscription '/topic/AllAccounts', replay: 10 do |message|
|
589
|
+
puts message.inspect
|
590
|
+
end
|
591
|
+
}
|
592
|
+
```
|
593
|
+
|
594
|
+
In this specific case you will see events with replay ID 11, 12 and so on.
|
595
|
+
|
596
|
+
There are two magic values for the replay ID accepted by Salesforce:
|
597
|
+
|
598
|
+
* `-2`, for getting all the events that appeared in the last 24 hours
|
599
|
+
* `-1`, for getting only newer events
|
600
|
+
|
601
|
+
**Warning**: Only use a replay ID of a event from the last 24 hours otherwise
|
602
|
+
Salesforce will not send anything, including newer events. If in doubt, use one
|
603
|
+
of the two magic replay IDs mentioned above.
|
604
|
+
|
605
|
+
You might want to store the replay ID in some sort of datastore so you can
|
606
|
+
access it, for example between application restarts. In that case, there is the
|
607
|
+
option of passing a custom replay handler which responds to `[]` and `[]=`.
|
608
|
+
|
609
|
+
Below is a sample replay handler that stores the replay ID for each channel in
|
610
|
+
memory using a Hash, stores a timestamp and has some rudimentary logic that
|
611
|
+
will use one of the magic IDs depending on the value of the timestamp:
|
612
|
+
|
613
|
+
```ruby
|
614
|
+
class SimpleReplayHandler
|
615
|
+
|
616
|
+
MAX_AGE = 86_400 # 24 hours
|
617
|
+
|
618
|
+
INIT_REPLAY_ID = -1
|
619
|
+
DEFAULT_REPLAY_ID = -2
|
620
|
+
|
621
|
+
def initialize
|
622
|
+
@channels = {}
|
623
|
+
@last_modified = nil
|
624
|
+
end
|
625
|
+
|
626
|
+
# This method is called during the initial subscribe phase
|
627
|
+
# in order to send the correct replay ID.
|
628
|
+
def [](channel)
|
629
|
+
if @last_modified.nil?
|
630
|
+
puts "[#{channel}] No timestamp defined, sending magic replay ID #{INIT_REPLAY_ID}"
|
631
|
+
|
632
|
+
INIT_REPLAY_ID
|
633
|
+
elsif old_replay_id?
|
634
|
+
puts "[#{channel}] Old timestamp, sending magic replay ID #{DEFAULT_REPLAY_ID}"
|
635
|
+
|
636
|
+
DEFAULT_REPLAY_ID
|
637
|
+
else
|
638
|
+
@channels[channel]
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
def []=(channel, replay_id)
|
643
|
+
puts "[#{channel}] Writing replay ID: #{replay_id}"
|
644
|
+
|
645
|
+
@last_modified = Time.now
|
646
|
+
@channels[channel] = replay_id
|
647
|
+
end
|
648
|
+
|
649
|
+
def old_replay_id?
|
650
|
+
@last_modified.is_a?(Time) && Time.now - @last_modified > MAX_AGE
|
651
|
+
end
|
652
|
+
end
|
653
|
+
```
|
654
|
+
|
655
|
+
In order to use it, simply pass the object as the value of the `replay` option
|
656
|
+
of the subscription:
|
657
|
+
|
658
|
+
```ruby
|
659
|
+
EM.run {
|
660
|
+
# Subscribe to the PushTopic and use the custom replay handler to store any
|
661
|
+
# received replay ID.
|
662
|
+
client.subscription '/topic/AllAccounts', replay: SimpleReplayHandler.new do |message|
|
663
|
+
puts message.inspect
|
664
|
+
end
|
665
|
+
}
|
666
|
+
```
|
667
|
+
|
668
|
+
_See also_:
|
669
|
+
|
670
|
+
* [Force.com Streaming API docs](http://www.salesforce.com/us/developer/docs/api_streaming/index.htm)
|
671
|
+
* [Message Durability docs](https://developer.salesforce.com/docs/atlas.en-us.api_streaming.meta/api_streaming/using_streaming_api_durability.htm)
|
551
672
|
|
552
673
|
*Note:* Restforce's streaming implementation is known to be compatible with version `0.8.9` of the faye gem.
|
553
674
|
|
@@ -576,7 +697,23 @@ client.without_caching do
|
|
576
697
|
end
|
577
698
|
```
|
578
699
|
|
579
|
-
|
700
|
+
If you prefer to opt in to caching on a per-request, you can do so by using .with_caching and
|
701
|
+
setting the `use_cache` config option to false:
|
702
|
+
|
703
|
+
```ruby
|
704
|
+
Restforce.configure do |config|
|
705
|
+
config.cache = Rails.cache
|
706
|
+
config.use_cache = false
|
707
|
+
end
|
708
|
+
```
|
709
|
+
|
710
|
+
```ruby
|
711
|
+
client.with_caching do
|
712
|
+
client.query('select Id from Account')
|
713
|
+
end
|
714
|
+
```
|
715
|
+
|
716
|
+
Caching is done based on your authentication credentials, so cached responses will not be shared between different Salesforce logins.
|
580
717
|
|
581
718
|
* * *
|
582
719
|
|
data/UPGRADING.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Upgrading from Restforce 4.x to 5.x
|
2
|
+
|
3
|
+
__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.
|
4
|
+
|
5
|
+
## Error classes are now defined up-front, rather than dynamically at runtime
|
6
|
+
|
7
|
+
__Likelyhood of impact__: Moderate
|
8
|
+
|
9
|
+
The Salesforce REST API can return a range of `errorCode`s representing different kinds of errors. To make these easy to
|
10
|
+
handle in your code, we want to turn these into individual, specific exception classes in the `Restforce::ErrorCode` namespace that inherit from `Restforce:: ResponseError`.
|
11
|
+
|
12
|
+
Up until now, these exception classes have been defined dynamically at runtime which has some disadvantages - see the [pull request](https://github.com/restforce/restforce/pull/551) for more details.
|
13
|
+
|
14
|
+
In this version, we switch to defining them up-front in the code based on a list in the Salesforce documentation. There is a risk that we might have missed some errors which should be defined. If any errors are missed, they will be added in patch versions (e.g. `5.0.1`).
|
15
|
+
|
16
|
+
If your application won't run because you are referring to an exception class that no longer exists, or you see warnings logged anywhere, please [create an issue](https://github.com/restforce/restforce/issues/new?template=unhandled-salesforce-error.md&title=Unhandled+Salesforce+error%3A+%3Cinsert+error+code+here%3E).
|
17
|
+
|
18
|
+
## Ruby 2.4 is no longer supported
|
19
|
+
|
20
|
+
__Likelyhood of impact__: Moderate
|
21
|
+
|
22
|
+
As of [5th April 2020](https://www.ruby-lang.org/en/news/2020/04/05/support-of-ruby-2-4-has-ended/), Ruby 2.4 is no longer officially supported as an active version of the Ruby language. That means that it will not receive patches and security fixes.
|
23
|
+
|
24
|
+
Accordingly, we've dropped support for Ruby 2.4 and earlier in the Restforce library. It *may* be compatible, bu we don't guarantee this or enforce it with automated tests.
|
25
|
+
|
26
|
+
Before you update to Restforce 5.x, you'll need to switch to Ruby 2.5 or later. The current version of Ruby at the time of wriing is 2.7.
|
27
|
+
|
28
|
+
## `Restforce::UnauthorizedError` no longer inherits from `Restforce::Error`
|
29
|
+
|
30
|
+
__Likelyhood of impact__: Low
|
31
|
+
|
32
|
+
Previously, the `Restforce::UnauthorizedError` returned when the library couldn't authenticate with the Salesforce API inherits from `Restforce::Error`. So, if you used `rescue Restforce::Error` in your code, you'd catch these exceptions.
|
33
|
+
|
34
|
+
We've now changed this exception class to inherit from `Faraday::ClientError` which allows the response body returned from the Salesforce API to be attached to the error.
|
35
|
+
|
36
|
+
If you refer to `Restforce::Error` anywhere in your code, you should check whether you also need to take into account `Restforce::UnauthorizedError`.
|
37
|
+
|
38
|
+
If you refer to `Faraday::ClientError` anywhere in your code, you should check that you want the case where Restforce can't authenticate to be included.
|
data/docker-compose.yml
ADDED
data/lib/restforce/attachment.rb
CHANGED
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
|
@@ -33,6 +33,11 @@ module Restforce
|
|
33
33
|
end
|
34
34
|
alias length size
|
35
35
|
|
36
|
+
# Returns true if the size of the Collection is zero.
|
37
|
+
def empty?
|
38
|
+
size.zero?
|
39
|
+
end
|
40
|
+
|
36
41
|
# Return array of the elements on the current page
|
37
42
|
def current_page
|
38
43
|
first(@raw_page['records'].size)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'erb'
|
3
4
|
require 'uri'
|
4
5
|
require 'restforce/concerns/verbs'
|
5
6
|
|
@@ -320,6 +321,7 @@ module Restforce
|
|
320
321
|
def update!(sobject, attrs)
|
321
322
|
id = attrs.fetch(attrs.keys.find { |k, v| k.to_s.casecmp('id').zero? }, nil)
|
322
323
|
raise ArgumentError, 'ID field missing from provided attributes' unless id
|
324
|
+
|
323
325
|
attrs_without_id = attrs.reject { |k, v| k.to_s.casecmp("id").zero? }
|
324
326
|
api_patch "sobjects/#{sobject}/#{CGI.escape(id)}", attrs_without_id
|
325
327
|
true
|
@@ -377,7 +379,8 @@ module Restforce
|
|
377
379
|
api_post "sobjects/#{sobject}/#{field}", attrs
|
378
380
|
end
|
379
381
|
else
|
380
|
-
api_patch "sobjects/#{sobject}/#{field}
|
382
|
+
api_patch "sobjects/#{sobject}/#{field}/" \
|
383
|
+
"#{ERB::Util.url_encode(external_id)}", attrs
|
381
384
|
end
|
382
385
|
|
383
386
|
response.body.respond_to?(:fetch) ? response.body.fetch('id', true) : true
|
@@ -414,7 +417,7 @@ module Restforce
|
|
414
417
|
# Returns true of the sobject was successfully deleted.
|
415
418
|
# Raises an exception if an error is returned from Salesforce.
|
416
419
|
def destroy!(sobject, id)
|
417
|
-
api_delete "sobjects/#{sobject}/#{
|
420
|
+
api_delete "sobjects/#{sobject}/#{ERB::Util.url_encode(id)}"
|
418
421
|
true
|
419
422
|
end
|
420
423
|
|
@@ -428,9 +431,9 @@ module Restforce
|
|
428
431
|
# Returns the Restforce::SObject sobject record.
|
429
432
|
def find(sobject, id, field = nil)
|
430
433
|
url = if field
|
431
|
-
"sobjects/#{sobject}/#{field}/#{
|
434
|
+
"sobjects/#{sobject}/#{field}/#{ERB::Util.url_encode(id)}"
|
432
435
|
else
|
433
|
-
"sobjects/#{sobject}/#{
|
436
|
+
"sobjects/#{sobject}/#{ERB::Util.url_encode(id)}"
|
434
437
|
end
|
435
438
|
api_get(url).body
|
436
439
|
end
|
@@ -446,9 +449,9 @@ module Restforce
|
|
446
449
|
#
|
447
450
|
def select(sobject, id, select, field = nil)
|
448
451
|
path = if field
|
449
|
-
"sobjects/#{sobject}/#{field}/#{
|
452
|
+
"sobjects/#{sobject}/#{field}/#{ERB::Util.url_encode(id)}"
|
450
453
|
else
|
451
|
-
"sobjects/#{sobject}/#{
|
454
|
+
"sobjects/#{sobject}/#{ERB::Util.url_encode(id)}"
|
452
455
|
end
|
453
456
|
|
454
457
|
path = "#{path}?fields=#{select.join(',')}" if select&.any?
|
@@ -507,7 +510,7 @@ module Restforce
|
|
507
510
|
|
508
511
|
# Internal: Errors that should be rescued from in non-bang methods
|
509
512
|
def exceptions
|
510
|
-
[Faraday::Error
|
513
|
+
[Faraday::Error]
|
511
514
|
end
|
512
515
|
end
|
513
516
|
end
|
@@ -19,6 +19,8 @@ module Restforce
|
|
19
19
|
Restforce::Middleware::Authentication::Password
|
20
20
|
elsif oauth_refresh?
|
21
21
|
Restforce::Middleware::Authentication::Token
|
22
|
+
elsif jwt?
|
23
|
+
Restforce::Middleware::Authentication::JWTBearer
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
@@ -38,6 +40,14 @@ module Restforce
|
|
38
40
|
options[:client_id] &&
|
39
41
|
options[:client_secret]
|
40
42
|
end
|
43
|
+
|
44
|
+
# Internal: Returns true if jwt bearer token flow should be used for
|
45
|
+
# authentication.
|
46
|
+
def jwt?
|
47
|
+
options[:jwt_key] &&
|
48
|
+
options[:username] &&
|
49
|
+
options[:client_id]
|
50
|
+
end
|
41
51
|
end
|
42
52
|
end
|
43
53
|
end
|
@@ -28,6 +28,8 @@ module Restforce
|
|
28
28
|
# password and oauth authentication
|
29
29
|
# :client_secret - The oauth client secret to use.
|
30
30
|
#
|
31
|
+
# :jwt_key - The private key for JWT authentication
|
32
|
+
#
|
31
33
|
# :host - The String hostname to use during
|
32
34
|
# authentication requests
|
33
35
|
# (default: 'login.salesforce.com').
|
@@ -59,9 +61,9 @@ module Restforce
|
|
59
61
|
def initialize(opts = {})
|
60
62
|
raise ArgumentError, 'Please specify a hash of options' unless opts.is_a?(Hash)
|
61
63
|
|
62
|
-
@options =
|
64
|
+
@options = Restforce.configuration.options.map do |option|
|
63
65
|
[option, Restforce.configuration.send(option)]
|
64
|
-
end
|
66
|
+
end.to_h
|
65
67
|
|
66
68
|
@options.merge! opts
|
67
69
|
yield builder if block_given?
|
@@ -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
|
@@ -72,9 +72,9 @@ module Restforce
|
|
72
72
|
# Internal: Faraday Connection options
|
73
73
|
def connection_options
|
74
74
|
{ request: {
|
75
|
-
|
76
|
-
|
77
|
-
},
|
75
|
+
timeout: options[:timeout],
|
76
|
+
open_timeout: options[:timeout]
|
77
|
+
},
|
78
78
|
proxy: options[:proxy_uri],
|
79
79
|
ssl: options[:ssl] }
|
80
80
|
end
|