active_force 0.25.0 → 0.27.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/CHANGELOG.md +6 -0
- data/README.md +45 -0
- data/lib/active_force/active_query.rb +20 -3
- data/lib/active_force/composite_batch_query.rb +42 -0
- data/lib/active_force/version.rb +1 -1
- data/lib/active_force.rb +9 -1
- data/spec/active_force/active_query_spec.rb +20 -0
- data/spec/active_force/composite_batch_query_spec.rb +101 -0
- data/spec/active_force/query_notifications_spec.rb +164 -0
- data/spec/active_force_spec.rb +20 -0
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c4cc417eaaf0ad9a412c1a39374d80e4a723d0f8d6184eba87c09371e3ba56a
|
|
4
|
+
data.tar.gz: d8cdb8bc16ab43f8d1fbe4840788e412d5aa0ad3a25734cbc2b85d7138626c75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c005126087818d2afa9ef019e537ae5920fea69f9275ba74293cbe97ea9335cfb62da1deedd1c58e707536357f45cf322dcfa48d38b6d007e89f5210dfaf86b2
|
|
7
|
+
data.tar.gz: 43623c0cda29edc52f3ccaddad50010a76d88417d6ebcbe5d80c2bd838518eddbaef217a41ec14c0e372ba50dbe8cad4cf3eaebe1f1f0998734bde19a4251d63
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Not released
|
|
4
4
|
|
|
5
|
+
## 0.27.0
|
|
6
|
+
- Emit `query.active_force` notifications for logical query executions, including aggregates and composite batch queries, with SOQL, model, client identity, transport, and standard exception payloads. Cache hits count as logical queries, not API calls. (https://github.com/Beyond-Finance/active_force/pull/117)
|
|
7
|
+
|
|
8
|
+
## 0.26.0
|
|
9
|
+
- Add ability to query using Composite Batch Api (https://github.com/Beyond-Finance/active_force/pull/114)
|
|
10
|
+
|
|
5
11
|
## 0.25.0
|
|
6
12
|
- Add support for Rails 8.x (https://github.com/Beyond-Finance/active_force/pull/108)
|
|
7
13
|
|
data/README.md
CHANGED
|
@@ -293,6 +293,47 @@ When using rails, you can generate a model with all the fields you have on your
|
|
|
293
293
|
|
|
294
294
|
rails g active_force:model <table name>
|
|
295
295
|
|
|
296
|
+
### Query notifications
|
|
297
|
+
|
|
298
|
+
ActiveForce emits `query.active_force` through `ActiveSupport::Notifications` for
|
|
299
|
+
logical query executions, including `count`, `sum`, and composite batch queries.
|
|
300
|
+
Requiring ActiveForce does not install a logger or query detector.
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
subscriber = ActiveSupport::Notifications.subscribe('query.active_force') do |*args|
|
|
304
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
305
|
+
# Inspect event.duration and event.payload in your development/test tooling.
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# When no longer needed:
|
|
309
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Subscriber callbacks run synchronously and are not isolated from the query
|
|
313
|
+
itself: if your subscriber raises, that exception replaces the query's real
|
|
314
|
+
result or real exception (this is standard `ActiveSupport::Notifications`
|
|
315
|
+
behavior, the same as Rails' own `sql.active_record`). Wrap any subscriber
|
|
316
|
+
logic that could fail — network calls, external clients — in its own `rescue`.
|
|
317
|
+
|
|
318
|
+
The payload contains:
|
|
319
|
+
|
|
320
|
+
- `soql`: the executed SOQL string, including raw values. Treat it as sensitive;
|
|
321
|
+
only log it deliberately with appropriate data handling.
|
|
322
|
+
- `model`: the queried `ActiveForce::SObject` class.
|
|
323
|
+
- `client_id`: the client's Ruby `object_id` (process-local identity, not credentials).
|
|
324
|
+
- `transport`: `:query` or `:composite_batch`. Aggregates retain direct `:query`
|
|
325
|
+
transport regardless of the composite threshold.
|
|
326
|
+
|
|
327
|
+
Failed executions include ActiveSupport's standard `exception` and
|
|
328
|
+
`exception_object` fields and still raise the original exception. Consumers
|
|
329
|
+
counting successful queries should exclude those events.
|
|
330
|
+
|
|
331
|
+
These are **logical queries, not Salesforce API-call counts**. Restforce HTTP-cache
|
|
332
|
+
hits still count; no cache-hit marker is exposed. Internal retries and later
|
|
333
|
+
pagination do not emit additional events. Lazy query construction and repeated
|
|
334
|
+
access to loaded records emit none. Instrumentation does not enumerate results.
|
|
335
|
+
Direct Restforce calls, SOSL, writes, and Bulk APIs are outside this event.
|
|
336
|
+
|
|
296
337
|
## Contributing
|
|
297
338
|
|
|
298
339
|
1. Fork it
|
|
@@ -308,3 +349,7 @@ When using rails, you can generate a model with all the fields you have on your
|
|
|
308
349
|
[3]: https://github.com/bkeepers/dotenv
|
|
309
350
|
[4]: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm
|
|
310
351
|
|
|
352
|
+
|
|
353
|
+
## License
|
|
354
|
+
|
|
355
|
+
[MIT](LICENSE.txt).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'active_support/all'
|
|
2
2
|
require 'active_force/query'
|
|
3
3
|
require 'active_force/select_builder'
|
|
4
|
+
require 'active_force/composite_batch_query'
|
|
4
5
|
require 'forwardable'
|
|
5
6
|
|
|
6
7
|
module ActiveForce
|
|
@@ -51,14 +52,14 @@ module ActiveForce
|
|
|
51
52
|
alias_method :all, :to_a
|
|
52
53
|
|
|
53
54
|
def count
|
|
54
|
-
|
|
55
|
+
execute_query(super.to_s).first.expr0
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def sum(field)
|
|
58
59
|
raise ArgumentError, 'field is required' if field.blank?
|
|
59
60
|
raise UnknownFieldError.new(sobject, field) unless mappings.key?(field.to_sym)
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
execute_query(super(mappings.fetch(field.to_sym)).to_s).first.expr0
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
def limit limit
|
|
@@ -245,7 +246,23 @@ module ActiveForce
|
|
|
245
246
|
end
|
|
246
247
|
|
|
247
248
|
def result
|
|
248
|
-
|
|
249
|
+
soql = self.to_s
|
|
250
|
+
|
|
251
|
+
transport = soql.length >= ActiveForce.composite_batch_query_threshold ? :composite_batch : :query
|
|
252
|
+
execute_query(soql, transport)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def execute_query(soql, transport = :query)
|
|
256
|
+
client = sfdc_client
|
|
257
|
+
payload = { soql: soql, model: sobject, client_id: client.object_id, transport: transport }
|
|
258
|
+
|
|
259
|
+
ActiveSupport::Notifications.instrument('query.active_force', payload) do
|
|
260
|
+
if transport == :composite_batch
|
|
261
|
+
CompositeBatchQuery.call(soql, client)
|
|
262
|
+
else
|
|
263
|
+
client.query(soql)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
249
266
|
end
|
|
250
267
|
|
|
251
268
|
def build_order_by(args)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'active_support/all'
|
|
2
|
+
|
|
3
|
+
module ActiveForce
|
|
4
|
+
class CompositeBatchQuery
|
|
5
|
+
def self.call(soql, sfdc_client = ActiveForce.sfdc_client)
|
|
6
|
+
new(soql, sfdc_client).call
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :sfdc_client, :soql
|
|
10
|
+
|
|
11
|
+
def initialize(soql, sfdc_client)
|
|
12
|
+
@sfdc_client = sfdc_client
|
|
13
|
+
@soql = soql
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
results = sfdc_client.batch do |subrequests|
|
|
18
|
+
subrequests.requests << {
|
|
19
|
+
method: "GET",
|
|
20
|
+
url: "v#{subrequests.options[:api_version]}/query?" + {q: soql}.to_query
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
r = results.first
|
|
25
|
+
|
|
26
|
+
process_composite_batch_error_response(r) if r.statusCode >= 300
|
|
27
|
+
|
|
28
|
+
r.result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def process_composite_batch_error_response(r)
|
|
34
|
+
response_value = {status: r.statusCode, body: r.result.as_json}
|
|
35
|
+
error_code = r.result.dig(0, "errorCode")
|
|
36
|
+
message = "#{error_code}: #{r.result.dig(0, "message")}"
|
|
37
|
+
message << "\nRESPONSE: #{r.result.to_json}"
|
|
38
|
+
|
|
39
|
+
raise Restforce::ErrorCode.get_exception_class(error_code).new(message, response_value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/active_force/version.rb
CHANGED
data/lib/active_force.rb
CHANGED
|
@@ -9,8 +9,16 @@ module ActiveForce
|
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
attr_accessor :sfdc_client
|
|
12
|
+
attr_writer :composite_batch_query_threshold
|
|
13
|
+
|
|
14
|
+
def composite_batch_query_threshold
|
|
15
|
+
@composite_batch_query_threshold ||= 100_000
|
|
16
|
+
|
|
17
|
+
return @composite_batch_query_threshold.call if @composite_batch_query_threshold.respond_to?(:call)
|
|
18
|
+
|
|
19
|
+
@composite_batch_query_threshold
|
|
20
|
+
end
|
|
12
21
|
end
|
|
13
22
|
|
|
14
23
|
self.sfdc_client = Restforce.new
|
|
15
|
-
|
|
16
24
|
end
|
|
@@ -557,6 +557,26 @@ describe ActiveForce::ActiveQuery do
|
|
|
557
557
|
end
|
|
558
558
|
end
|
|
559
559
|
|
|
560
|
+
describe '#result routing' do
|
|
561
|
+
after do
|
|
562
|
+
ActiveForce.instance_variable_set(:@composite_batch_query_threshold, nil)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
it 'uses sfdc_client.query when SOQL is at or below the threshold' do
|
|
566
|
+
ActiveForce.composite_batch_query_threshold = 100_000
|
|
567
|
+
expect(client).to receive(:query).and_return([])
|
|
568
|
+
expect(ActiveForce::CompositeBatchQuery).not_to receive(:call)
|
|
569
|
+
active_query.where("Id = 'foo'").to_a
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
it 'uses CompositeBatchQuery.call when SOQL exceeds the threshold' do
|
|
573
|
+
ActiveForce.composite_batch_query_threshold = 0
|
|
574
|
+
expect(ActiveForce::CompositeBatchQuery).to receive(:call).and_return([])
|
|
575
|
+
expect(client).not_to receive(:query)
|
|
576
|
+
active_query.where("Id = 'foo'").to_a
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
560
580
|
describe "#order" do
|
|
561
581
|
context 'when it is symbol' do
|
|
562
582
|
it "should add an order condition with actual SF field name" do
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe ActiveForce::CompositeBatchQuery do
|
|
4
|
+
let(:soql) { "SELECT Id FROM Account WHERE Name = 'Test'" }
|
|
5
|
+
let(:api_version) { '58.0' }
|
|
6
|
+
let(:sfdc_client) { double(Restforce) }
|
|
7
|
+
let(:subrequests) do
|
|
8
|
+
double('subrequests', requests: [], options: { api_version: api_version })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe '.call' do
|
|
12
|
+
it 'delegates to new(...).call' do
|
|
13
|
+
result_record = double('result_record')
|
|
14
|
+
instance = instance_double(described_class)
|
|
15
|
+
allow(described_class).to receive(:new).with(soql, sfdc_client).and_return(instance)
|
|
16
|
+
allow(instance).to receive(:call).and_return(result_record)
|
|
17
|
+
|
|
18
|
+
expect(described_class.call(soql, sfdc_client)).to eq(result_record)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'defaults to ActiveForce.sfdc_client when no client is provided' do
|
|
22
|
+
default_client = double(Restforce)
|
|
23
|
+
allow(ActiveForce).to receive(:sfdc_client).and_return(default_client)
|
|
24
|
+
|
|
25
|
+
instance = instance_double(described_class)
|
|
26
|
+
allow(described_class).to receive(:new).with(soql, default_client).and_return(instance)
|
|
27
|
+
allow(instance).to receive(:call).and_return(double('result'))
|
|
28
|
+
|
|
29
|
+
expect { described_class.call(soql) }.not_to raise_error
|
|
30
|
+
expect(described_class).to have_received(:new).with(soql, default_client)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#call' do
|
|
35
|
+
context 'when the response is successful' do
|
|
36
|
+
let(:result_record) { double(Restforce::Collection) }
|
|
37
|
+
let(:batch_result) do
|
|
38
|
+
double(Restforce::Mash, statusCode: 200, result: result_record)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
before do
|
|
42
|
+
allow(sfdc_client).to receive(:batch).and_yield(subrequests).and_return([batch_result])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'sends a GET batch subrequest with the correct URL' do
|
|
46
|
+
described_class.call(soql, sfdc_client)
|
|
47
|
+
|
|
48
|
+
expected_url = "v#{api_version}/query?" + { q: soql }.to_query
|
|
49
|
+
expect(subrequests.requests).to include(
|
|
50
|
+
hash_including(method: 'GET', url: expected_url)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns the result from the batch response' do
|
|
55
|
+
expect(described_class.call(soql, sfdc_client)).to eq(result_record)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context 'when the response statusCode is exactly 300' do
|
|
60
|
+
let(:error_body) do
|
|
61
|
+
[{ 'errorCode' => 'MULTIPLE_CHOICES', 'message' => 'multiple records found' }]
|
|
62
|
+
end
|
|
63
|
+
let(:batch_result) do
|
|
64
|
+
double('batch_result', statusCode: 300, result: error_body)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
before do
|
|
68
|
+
allow(sfdc_client).to receive(:batch).and_yield(subrequests).and_return([batch_result])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'treats it as an error' do
|
|
72
|
+
expect {
|
|
73
|
+
described_class.call(soql, sfdc_client)
|
|
74
|
+
}.to raise_error(Restforce::ResponseError)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context 'when the response is an error' do
|
|
79
|
+
let(:error_body) do
|
|
80
|
+
[{ 'errorCode' => 'MALFORMED_QUERY', 'message' => 'unexpected token' }]
|
|
81
|
+
end
|
|
82
|
+
let(:batch_result) do
|
|
83
|
+
double(Restforce::Mash, statusCode: 400, result: error_body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
before do
|
|
87
|
+
allow(sfdc_client).to receive(:batch).and_yield(subrequests).and_return([batch_result])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'raises a Restforce error with the correct error code and message' do
|
|
91
|
+
expect {
|
|
92
|
+
described_class.call(soql, sfdc_client)
|
|
93
|
+
}.to raise_error(Restforce::ErrorCode::MalformedQuery) do |error|
|
|
94
|
+
expect(error.message).to include('MALFORMED_QUERY')
|
|
95
|
+
expect(error.message).to include('unexpected token')
|
|
96
|
+
expect(error.message).to include('RESPONSE:')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'query.active_force notifications' do
|
|
4
|
+
let(:model) do
|
|
5
|
+
Class.new(ActiveForce::SObject) do
|
|
6
|
+
self.table_name = 'Widgets'
|
|
7
|
+
field :id, from: 'Id'
|
|
8
|
+
field :amount, from: 'Amount__c'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
let(:client) { double('client') }
|
|
12
|
+
let(:query) { ActiveForce::ActiveQuery.new(model) }
|
|
13
|
+
let(:events) { [] }
|
|
14
|
+
|
|
15
|
+
around do |example|
|
|
16
|
+
subscriber = ActiveSupport::Notifications.subscribe('query.active_force') do |*args|
|
|
17
|
+
events << ActiveSupport::Notifications::Event.new(*args)
|
|
18
|
+
end
|
|
19
|
+
example.run
|
|
20
|
+
ensure
|
|
21
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
before do
|
|
25
|
+
allow(model).to receive(:sfdc_client).and_return(client)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'emits the SOQL, model, nonsecret client identity and direct transport without enumerating the result' do
|
|
29
|
+
result = double('unloaded collection')
|
|
30
|
+
expect(client).to receive(:query).with(query.to_s).and_return(result)
|
|
31
|
+
expect(query.send(:result)).to equal(result)
|
|
32
|
+
expect(events.size).to eq(1)
|
|
33
|
+
expect(events.first.payload).to eq(soql: query.to_s, model: model,
|
|
34
|
+
client_id: client.object_id, transport: :query)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'does not emit for lazy construction or SOQL rendering' do
|
|
38
|
+
query.where(id: 'fake-id').select(:id).to_s
|
|
39
|
+
expect(events).to be_empty
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'does not emit again when loaded records are reused' do
|
|
43
|
+
expect(client).to receive(:query).once.and_return([])
|
|
44
|
+
result = query.to_a
|
|
45
|
+
expect(query.to_a).to equal(result)
|
|
46
|
+
expect(events.size).to eq(1)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'counts separate logical queries even when a client returns the same cached result' do
|
|
50
|
+
cached_result = []
|
|
51
|
+
expect(client).to receive(:query).twice.and_return(cached_result)
|
|
52
|
+
2.times { ActiveForce::ActiveQuery.new(model).to_a }
|
|
53
|
+
expect(events.size).to eq(2)
|
|
54
|
+
expect(events.map(&:payload).uniq.size).to eq(1)
|
|
55
|
+
expect(events.first.payload).not_to have_key(:cache_hit)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'does not emit for pages retrieved while enumerating the returned collection' do
|
|
59
|
+
collection = double('paginated collection')
|
|
60
|
+
expect(client).to receive(:query).once.and_return(collection)
|
|
61
|
+
expect(collection).to receive(:to_a) do
|
|
62
|
+
client.get('/fake/next-page')
|
|
63
|
+
[]
|
|
64
|
+
end
|
|
65
|
+
expect(client).to receive(:get).with('/fake/next-page')
|
|
66
|
+
query.to_a
|
|
67
|
+
expect(events.size).to eq(1)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'emits once when the client retries internally' do
|
|
71
|
+
attempts = 0
|
|
72
|
+
allow(client).to receive(:query) do
|
|
73
|
+
begin
|
|
74
|
+
attempts += 1
|
|
75
|
+
raise IOError if attempts == 1
|
|
76
|
+
[]
|
|
77
|
+
rescue IOError
|
|
78
|
+
retry
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
query.to_a
|
|
82
|
+
expect(attempts).to eq(2)
|
|
83
|
+
expect(events.size).to eq(1)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'distinguishes client instances' do
|
|
87
|
+
other_client = double('other client', query: [])
|
|
88
|
+
allow(client).to receive(:query).and_return([])
|
|
89
|
+
query.to_a
|
|
90
|
+
allow(model).to receive(:sfdc_client).and_return(other_client)
|
|
91
|
+
ActiveForce::ActiveQuery.new(model).to_a
|
|
92
|
+
expect(events.map { |event| event.payload[:client_id] }).to eq([client.object_id, other_client.object_id])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
[:query, :composite_batch].each do |transport|
|
|
96
|
+
context "with #{transport} transport" do
|
|
97
|
+
before do
|
|
98
|
+
allow(ActiveForce).to receive(:composite_batch_query_threshold).and_return(transport == :query ? 100_000 : 0)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'preserves the result and emits one event' do
|
|
102
|
+
result = double('unloaded result')
|
|
103
|
+
if transport == :query
|
|
104
|
+
expect(client).to receive(:query).with(query.to_s).and_return(result)
|
|
105
|
+
else
|
|
106
|
+
expect(client).not_to receive(:query)
|
|
107
|
+
expect(ActiveForce::CompositeBatchQuery).to receive(:call).with(query.to_s, client).and_return(result)
|
|
108
|
+
end
|
|
109
|
+
expect(query.send(:result)).to equal(result)
|
|
110
|
+
expect(events.size).to eq(1)
|
|
111
|
+
expect(events.first.payload[:transport]).to eq(transport)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'preserves the original exception and standard notification error payload' do
|
|
115
|
+
error = IOError.new('fake failure')
|
|
116
|
+
if transport == :query
|
|
117
|
+
allow(client).to receive(:query).and_raise(error)
|
|
118
|
+
else
|
|
119
|
+
allow(ActiveForce::CompositeBatchQuery).to receive(:call).and_raise(error)
|
|
120
|
+
end
|
|
121
|
+
expect { query.to_a }.to raise_error { |raised| expect(raised).to equal(error) }
|
|
122
|
+
expect(events.size).to eq(1)
|
|
123
|
+
expect(events.first.payload).to include(exception: ['IOError', 'fake failure'], exception_object: error,
|
|
124
|
+
transport: transport, soql: query.to_s, model: model, client_id: client.object_id)
|
|
125
|
+
expect(query.loaded?).to be false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
[:count, :sum].each do |operation|
|
|
131
|
+
context "##{operation}" do
|
|
132
|
+
let(:arguments) { operation == :sum ? [:amount] : [] }
|
|
133
|
+
|
|
134
|
+
before do
|
|
135
|
+
allow(ActiveForce).to receive(:composite_batch_query_threshold).and_return(0)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'retains direct query transport and the aggregate value even above the composite threshold' do
|
|
139
|
+
expect(ActiveForce::CompositeBatchQuery).not_to receive(:call)
|
|
140
|
+
aggregate = double('aggregate', expr0: 42)
|
|
141
|
+
expect(client).to receive(:query).and_return([aggregate])
|
|
142
|
+
expect(query.public_send(operation, *arguments)).to eq(42)
|
|
143
|
+
expect(events.size).to eq(1)
|
|
144
|
+
expression = operation == :sum ? 'sum(Amount__c)' : 'count(Id)'
|
|
145
|
+
expect(events.first.payload).to eq(soql: "SELECT #{expression} FROM Widgets", model: model,
|
|
146
|
+
client_id: client.object_id, transport: :query)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'reports failed aggregate executions without replacing the exception' do
|
|
150
|
+
error = IOError.new('fake aggregate failure')
|
|
151
|
+
allow(client).to receive(:query).and_raise(error)
|
|
152
|
+
expect { query.public_send(operation, *arguments) }.to raise_error { |raised| expect(raised).to equal(error) }
|
|
153
|
+
expect(events.size).to eq(1)
|
|
154
|
+
expect(events.first.payload[:exception_object]).to equal(error)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'does not emit for invalid sum arguments' do
|
|
160
|
+
expect { query.sum(nil) }.to raise_error(ArgumentError)
|
|
161
|
+
expect { query.sum(:unknown) }.to raise_error(ActiveForce::UnknownFieldError)
|
|
162
|
+
expect(events).to be_empty
|
|
163
|
+
end
|
|
164
|
+
end
|
data/spec/active_force_spec.rb
CHANGED
|
@@ -4,4 +4,24 @@ describe ActiveForce do
|
|
|
4
4
|
it 'should have a version number' do
|
|
5
5
|
expect(ActiveForce::VERSION).to_not be_nil
|
|
6
6
|
end
|
|
7
|
+
|
|
8
|
+
describe '.composite_batch_query_threshold' do
|
|
9
|
+
after do
|
|
10
|
+
ActiveForce.instance_variable_set(:@composite_batch_query_threshold, nil)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'returns 100_000 by default' do
|
|
14
|
+
expect(ActiveForce.composite_batch_query_threshold).to eq(100_000)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'allows setting a custom integer value' do
|
|
18
|
+
ActiveForce.composite_batch_query_threshold = 25_000
|
|
19
|
+
expect(ActiveForce.composite_batch_query_threshold).to eq(25_000)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'supports callable values (proc/lambda)' do
|
|
23
|
+
ActiveForce.composite_batch_query_threshold = -> { 50_000 }
|
|
24
|
+
expect(ActiveForce.composite_batch_query_threshold).to eq(50_000)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
7
27
|
end
|
metadata
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_force
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eloy Espinaco
|
|
8
8
|
- Pablo Oldani
|
|
9
9
|
- Armando Andini
|
|
10
10
|
- José Piccioni
|
|
11
|
-
autorequire:
|
|
12
11
|
bindir: bin
|
|
13
12
|
cert_chain: []
|
|
14
|
-
date:
|
|
13
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
15
14
|
dependencies:
|
|
16
15
|
- !ruby/object:Gem::Dependency
|
|
17
16
|
name: activemodel
|
|
@@ -173,6 +172,7 @@ files:
|
|
|
173
172
|
- lib/active_force/bulk/job.rb
|
|
174
173
|
- lib/active_force/bulk/job_result.rb
|
|
175
174
|
- lib/active_force/bulk/records.rb
|
|
175
|
+
- lib/active_force/composite_batch_query.rb
|
|
176
176
|
- lib/active_force/field.rb
|
|
177
177
|
- lib/active_force/mapping.rb
|
|
178
178
|
- lib/active_force/query.rb
|
|
@@ -195,8 +195,10 @@ files:
|
|
|
195
195
|
- spec/active_force/bulk/record_spec.rb
|
|
196
196
|
- spec/active_force/bulk_spec.rb
|
|
197
197
|
- spec/active_force/callbacks_spec.rb
|
|
198
|
+
- spec/active_force/composite_batch_query_spec.rb
|
|
198
199
|
- spec/active_force/field_spec.rb
|
|
199
200
|
- spec/active_force/mapping_spec.rb
|
|
201
|
+
- spec/active_force/query_notifications_spec.rb
|
|
200
202
|
- spec/active_force/query_spec.rb
|
|
201
203
|
- spec/active_force/sobject/includes_spec.rb
|
|
202
204
|
- spec/active_force/sobject/select_spec.rb
|
|
@@ -218,7 +220,6 @@ metadata:
|
|
|
218
220
|
bug_tracker_uri: https://github.com/Beyond-Finance/active_force/issues
|
|
219
221
|
changelog_uri: https://github.com/Beyond-Finance/active_force/blob/main/CHANGELOG.md
|
|
220
222
|
source_code_uri: https://github.com/Beyond-Finance/active_force
|
|
221
|
-
post_install_message:
|
|
222
223
|
rdoc_options: []
|
|
223
224
|
require_paths:
|
|
224
225
|
- lib
|
|
@@ -233,8 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
233
234
|
- !ruby/object:Gem::Version
|
|
234
235
|
version: '0'
|
|
235
236
|
requirements: []
|
|
236
|
-
rubygems_version:
|
|
237
|
-
signing_key:
|
|
237
|
+
rubygems_version: 4.0.16
|
|
238
238
|
specification_version: 4
|
|
239
239
|
summary: Help you implement models persisting on Sales Force within Rails using RESTForce
|
|
240
240
|
test_files:
|
|
@@ -246,8 +246,10 @@ test_files:
|
|
|
246
246
|
- spec/active_force/bulk/record_spec.rb
|
|
247
247
|
- spec/active_force/bulk_spec.rb
|
|
248
248
|
- spec/active_force/callbacks_spec.rb
|
|
249
|
+
- spec/active_force/composite_batch_query_spec.rb
|
|
249
250
|
- spec/active_force/field_spec.rb
|
|
250
251
|
- spec/active_force/mapping_spec.rb
|
|
252
|
+
- spec/active_force/query_notifications_spec.rb
|
|
251
253
|
- spec/active_force/query_spec.rb
|
|
252
254
|
- spec/active_force/sobject/includes_spec.rb
|
|
253
255
|
- spec/active_force/sobject/select_spec.rb
|