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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2717b024f19c2c0dbf7530d606797a5880dfe629a154e6eebd083da381b917b1
4
- data.tar.gz: ab29915f960873acd523a12f73af63f8c13b200e8cddbe4465ef28038cdebf2e
3
+ metadata.gz: 1c4cc417eaaf0ad9a412c1a39374d80e4a723d0f8d6184eba87c09371e3ba56a
4
+ data.tar.gz: d8cdb8bc16ab43f8d1fbe4840788e412d5aa0ad3a25734cbc2b85d7138626c75
5
5
  SHA512:
6
- metadata.gz: dc4aa9e5110884da1008add3a996387d66af6c337328d56e773457fb24e61410a63d5bd3a734ab5bf79a44c4b5962a55aca1c00314af41d1275df663fa133f16
7
- data.tar.gz: 6702930b7f9d47c2290c4e00300160f5e3261ed9c22834ab4bef55a8438f6ec778f97e51aab197b23d10b1d2b6c6d20dc3e2991b728a847529cf16e7121d6983
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
- sfdc_client.query(super.to_s).first.expr0
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
- sfdc_client.query(super(mappings.fetch(field.to_sym)).to_s).first.expr0
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
- sfdc_client.query(self.to_s)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveForce
4
- VERSION = '0.25.0'
4
+ VERSION = '0.27.0'
5
5
  end
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
@@ -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.25.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: 2025-08-07 00:00:00.000000000 Z
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: 3.4.10
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