restforce 5.1.1 → 5.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68aba674731ec407d63a766c8bcf13a6a243e757e89b32c8bed07c07a2ee2a9e
4
- data.tar.gz: 1529b415da1c3d34ff477aea87116d147d5e2bd3e74ebde236ef9292545fad54
3
+ metadata.gz: f30c54c95fc530b6278faf751728b2793971c8aa0e43862f6719753a1d62d4fa
4
+ data.tar.gz: c7ac3a654170e2cb883325b12f4e6315d0c0423f9673142c93c87cf5de2003b8
5
5
  SHA512:
6
- metadata.gz: 5fc24ced1a9e38b622040d0b51ea3cd3aa2d4afc994ee41a8825ee2038c1f7379c56c2a32044a938102fe47030f02183549bdd91837ffab88320d99af13d27c5
7
- data.tar.gz: 1a0be3c634cb0b9bcea1b38463b09e62e54ceee49b44e30e852e42c14c91761eff4cf8a6a786611682cb0ba309704d85243499c00358b6e411ded15e805f421e
6
+ metadata.gz: 4a731ebfe6e755b66479f8e85434a017d50a81c084f048e1454f3714bb047c36f8362976c904a91890b8f62099a3238689fe93f200468670b8f4dafc51cb5aa5
7
+ data.tar.gz: f5e8cd87fc4eccd486828627dfa1c769107e5869f7cf60088d5c7ee2cde2207d9bf33794a4f1639b98cfccfee54734d583dcbdbea8d841442a7086ff00b9e315
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 5.2.0 (Oct 15, 2021)
2
+
3
+ * Add support for Salesforce's Composite API and Composite Batch API (@meenie, @amacdougall)
4
+ * Improve the performance of counting numbers of query results with `Restforce::Collection#count`, avoiding unnecessary API requests (@jhass)
5
+
1
6
  ## 5.1.1 (Oct 13, 2021)
2
7
 
3
8
  * Handle the `INVALID_REPLICATION_DATE` error returned by Salesforce (@michaelwnyc)
data/README.md CHANGED
@@ -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.1.1'
30
+ gem 'restforce', '~> 5.2.0'
29
31
 
30
32
  And then execute:
31
33
 
@@ -573,6 +575,52 @@ end
573
575
  Boom, you're now receiving push notifications when Accounts are
574
576
  created/updated.
575
577
 
578
+ #### Composite API
579
+
580
+ Restforce supports the [Composite API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_composite.htm).
581
+ This feature permits the user to send a composite object—that is, a complex
582
+ object with nested children—in a single API call. Up to 25 requests may be
583
+ included in a single composite.
584
+
585
+ Note that `GET` is not yet implemented for this API.
586
+
587
+ ```ruby
588
+ # build up an array of requests:
589
+ requests << {
590
+ method: :update,
591
+ sobject: sobject, # e.g. "Contact"
592
+ reference_id: reference_id,
593
+ data: data
594
+ }
595
+
596
+ # send every 25 requests as a subrequest in a single composite call
597
+ requests.each_slice(25).map do |req_slice|
598
+ client.composite do |subrequest|
599
+ req_slice.each do |r|
600
+ subrequest.send *r.values
601
+ end
602
+ end
603
+ end
604
+
605
+ # note that we're using `map` to return an array of each responses to each
606
+ # composite call; 100 requests will produce 4 responses
607
+ ```
608
+
609
+ #### Composite Batch API
610
+
611
+ Restforce supports the [Composite Batch API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_batch.htm).
612
+ This feature permits up to 25 subrequests in a single request, though each
613
+ subrequest counts against the API limit. On the other hand, it has fewer
614
+ limitations than the Composite API.
615
+
616
+ ```
617
+ client.batch do |subrequests|
618
+ subrequests.create('Object', name: 'test')
619
+ subrequests.update('Object', id: '123', name: 'test')
620
+ subrequests.destroy('Object', '123')
621
+ end
622
+ ```
623
+
576
624
  #### Replaying Events
577
625
 
578
626
  Since API version 37.0, Salesforce stores events for 24 hours and they can be
@@ -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,26 @@ 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.
31
33
  def size
32
34
  @raw_page['totalSize']
33
35
  end
34
36
  alias length size
35
37
 
38
+ def count(*args)
39
+ # By default, `Enumerable`'s `#count` uses `#each`, which means going through all
40
+ # of the pages of results, one by one. Instead, we can use `#size` which we have
41
+ # already overridden to work in a smarter, more efficient way. This only works for
42
+ # the simple version of `#count` with no arguments. When called with an argument or
43
+ # a block, you need to know what the items in the collection actually are, so we
44
+ # call `super` and end up iterating through each item in the collection.
45
+ return size unless block_given? || !args.empty?
46
+
47
+ super
48
+ end
49
+
36
50
  # Returns true if the size of the Collection is zero.
37
51
  def empty?
38
52
  size.zero?
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restforce
4
- VERSION = '5.1.1'
4
+ VERSION = '5.2.0'
5
5
  end
data/lib/restforce.rb CHANGED
@@ -32,6 +32,7 @@ module Restforce
32
32
  autoload :Base, 'restforce/concerns/base'
33
33
  autoload :API, 'restforce/concerns/api'
34
34
  autoload :BatchAPI, 'restforce/concerns/batch_api'
35
+ autoload :CompositeAPI, 'restforce/concerns/composite_api'
35
36
  end
36
37
 
37
38
  module Data
@@ -48,6 +49,7 @@ module Restforce
48
49
  UnauthorizedError = Class.new(Faraday::ClientError)
49
50
  APIVersionError = Class.new(Error)
50
51
  BatchAPIError = Class.new(Error)
52
+ CompositeAPIError = Class.new(Error)
51
53
 
52
54
  # Inherit from Faraday::ResourceNotFound for backwards-compatibility
53
55
  # Consumers of this library that rescue and handle Faraday::ResourceNotFound
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restforce::Concerns::CompositeAPI do
6
+ let(:endpoint) { 'composite' }
7
+
8
+ before do
9
+ client.should_receive(:options).and_return(api_version: 38.0)
10
+ end
11
+
12
+ shared_examples_for 'composite requests' do
13
+ it '#create' do
14
+ client.
15
+ should_receive(:api_post).
16
+ with(endpoint, { compositeRequest: [
17
+ {
18
+ method: 'POST',
19
+ url: '/services/data/v38.0/sobjects/Object',
20
+ body: { name: 'test' },
21
+ referenceId: 'create_ref'
22
+ }
23
+ ], allOrNone: all_or_none, collateSubrequests: false }.to_json).
24
+ and_return(response)
25
+
26
+ client.send(method) do |subrequests|
27
+ subrequests.create('Object', 'create_ref', name: 'test')
28
+ end
29
+ end
30
+
31
+ it '#update' do
32
+ client.
33
+ should_receive(:api_post).
34
+ with(endpoint, { compositeRequest: [
35
+ {
36
+ method: 'PATCH',
37
+ url: '/services/data/v38.0/sobjects/Object/123',
38
+ body: { name: 'test' },
39
+ referenceId: 'update_ref'
40
+ }
41
+ ], allOrNone: all_or_none, collateSubrequests: false }.to_json).
42
+ and_return(response)
43
+
44
+ client.send(method) do |subrequests|
45
+ subrequests.update('Object', 'update_ref', id: '123', name: 'test')
46
+ end
47
+ end
48
+
49
+ it '#destroy' do
50
+ client.
51
+ should_receive(:api_post).
52
+ with(endpoint, { compositeRequest: [
53
+ {
54
+ method: 'DELETE',
55
+ url: '/services/data/v38.0/sobjects/Object/123',
56
+ referenceId: 'destroy_ref'
57
+ }
58
+ ], allOrNone: all_or_none, collateSubrequests: false }.to_json).
59
+ and_return(response)
60
+
61
+ client.send(method) do |subrequests|
62
+ subrequests.destroy('Object', 'destroy_ref', '123')
63
+ end
64
+ end
65
+
66
+ it '#upsert' do
67
+ client.
68
+ should_receive(:api_post).
69
+ with(endpoint, { compositeRequest: [
70
+ {
71
+ method: 'PATCH',
72
+ url: '/services/data/v38.0/sobjects/Object/extIdField__c/456',
73
+ body: { name: 'test' },
74
+ referenceId: 'upsert_ref'
75
+ }
76
+ ], allOrNone: all_or_none, collateSubrequests: false }.to_json).
77
+ and_return(response)
78
+
79
+ client.send(method) do |subrequests|
80
+ subrequests.upsert('Object', 'upsert_ref', 'extIdField__c',
81
+ extIdField__c: '456', name: 'test')
82
+ end
83
+ end
84
+
85
+ it 'multiple subrequests' do
86
+ client.
87
+ should_receive(:api_post).
88
+ with(endpoint, { compositeRequest: [
89
+ {
90
+ method: 'POST',
91
+ url: '/services/data/v38.0/sobjects/Object',
92
+ body: { name: 'test' },
93
+ referenceId: 'create_ref'
94
+ },
95
+ {
96
+ method: 'PATCH',
97
+ url: '/services/data/v38.0/sobjects/Object/123',
98
+ body: { name: 'test' },
99
+ referenceId: 'update_ref'
100
+ },
101
+ {
102
+ method: 'DELETE',
103
+ url: '/services/data/v38.0/sobjects/Object/123',
104
+ referenceId: 'destroy_ref'
105
+ }
106
+ ], allOrNone: all_or_none, collateSubrequests: false }.to_json).
107
+ and_return(response)
108
+
109
+ client.send(method) do |subrequests|
110
+ subrequests.create('Object', 'create_ref', name: 'test')
111
+ subrequests.update('Object', 'update_ref', id: '123', name: 'test')
112
+ subrequests.destroy('Object', 'destroy_ref', '123')
113
+ end
114
+ end
115
+
116
+ it 'fails if more than 25 requests' do
117
+ expect do
118
+ client.send(method) do |subrequests|
119
+ 26.times do |i|
120
+ subrequests.upsert('Object', "upsert_ref_#{i}", 'extIdField__c',
121
+ extIdField__c: '456', name: 'test')
122
+ end
123
+ end
124
+ end.to raise_error(ArgumentError)
125
+ end
126
+ end
127
+
128
+ describe '#composite' do
129
+ let(:method) { :composite }
130
+ let(:all_or_none) { false }
131
+ let(:response) { double('Faraday::Response', body: { 'CompositeResponse' => [] }) }
132
+ it_behaves_like 'composite requests'
133
+ end
134
+
135
+ describe '#composite!' do
136
+ let(:method) { :composite! }
137
+ let(:all_or_none) { true }
138
+ let(:response) do
139
+ double('Faraday::Response', body: { 'CompositeResponse' => [] })
140
+ end
141
+ it_behaves_like 'composite requests'
142
+ end
143
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restforce
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.1
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Rogers
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-10-13 00:00:00.000000000 Z
12
+ date: 2021-10-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -121,6 +121,7 @@ files:
121
121
  - lib/restforce/concerns/batch_api.rb
122
122
  - lib/restforce/concerns/caching.rb
123
123
  - lib/restforce/concerns/canvas.rb
124
+ - lib/restforce/concerns/composite_api.rb
124
125
  - lib/restforce/concerns/connection.rb
125
126
  - lib/restforce/concerns/picklists.rb
126
127
  - lib/restforce/concerns/streaming.rb
@@ -209,6 +210,7 @@ files:
209
210
  - spec/unit/concerns/batch_api_spec.rb
210
211
  - spec/unit/concerns/caching_spec.rb
211
212
  - spec/unit/concerns/canvas_spec.rb
213
+ - spec/unit/concerns/composite_api_spec.rb
212
214
  - spec/unit/concerns/connection_spec.rb
213
215
  - spec/unit/concerns/streaming_spec.rb
214
216
  - spec/unit/config_spec.rb
@@ -310,6 +312,7 @@ test_files:
310
312
  - spec/unit/concerns/batch_api_spec.rb
311
313
  - spec/unit/concerns/caching_spec.rb
312
314
  - spec/unit/concerns/canvas_spec.rb
315
+ - spec/unit/concerns/composite_api_spec.rb
313
316
  - spec/unit/concerns/connection_spec.rb
314
317
  - spec/unit/concerns/streaming_spec.rb
315
318
  - spec/unit/config_spec.rb