active_force 0.26.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 +3 -0
- data/README.md +45 -0
- data/lib/active_force/active_query.rb +16 -6
- data/lib/active_force/version.rb +1 -1
- data/spec/active_force/query_notifications_spec.rb +164 -0
- metadata +4 -2
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,9 @@
|
|
|
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
|
+
|
|
5
8
|
## 0.26.0
|
|
6
9
|
- Add ability to query using Composite Batch Api (https://github.com/Beyond-Finance/active_force/pull/114)
|
|
7
10
|
|
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).
|
|
@@ -52,14 +52,14 @@ module ActiveForce
|
|
|
52
52
|
alias_method :all, :to_a
|
|
53
53
|
|
|
54
54
|
def count
|
|
55
|
-
|
|
55
|
+
execute_query(super.to_s).first.expr0
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def sum(field)
|
|
59
59
|
raise ArgumentError, 'field is required' if field.blank?
|
|
60
60
|
raise UnknownFieldError.new(sobject, field) unless mappings.key?(field.to_sym)
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
execute_query(super(mappings.fetch(field.to_sym)).to_s).first.expr0
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def limit limit
|
|
@@ -248,10 +248,20 @@ module ActiveForce
|
|
|
248
248
|
def result
|
|
249
249
|
soql = self.to_s
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
255
265
|
end
|
|
256
266
|
end
|
|
257
267
|
|
data/lib/active_force/version.rb
CHANGED
|
@@ -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
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
@@ -198,6 +198,7 @@ files:
|
|
|
198
198
|
- spec/active_force/composite_batch_query_spec.rb
|
|
199
199
|
- spec/active_force/field_spec.rb
|
|
200
200
|
- spec/active_force/mapping_spec.rb
|
|
201
|
+
- spec/active_force/query_notifications_spec.rb
|
|
201
202
|
- spec/active_force/query_spec.rb
|
|
202
203
|
- spec/active_force/sobject/includes_spec.rb
|
|
203
204
|
- spec/active_force/sobject/select_spec.rb
|
|
@@ -233,7 +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
|
+
rubygems_version: 4.0.16
|
|
237
238
|
specification_version: 4
|
|
238
239
|
summary: Help you implement models persisting on Sales Force within Rails using RESTForce
|
|
239
240
|
test_files:
|
|
@@ -248,6 +249,7 @@ test_files:
|
|
|
248
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
|