forest_liana 9.20.11 → 9.20.12

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: a13f2892e518d41f6c0aae28e4f14397086cd58f19b25c4b1fa86b4fefeaf911
4
- data.tar.gz: 76853d12f9eddbf87235f2a407814d3e23938a35f51c1e77a33f99ccdee0a5e7
3
+ metadata.gz: 113a6c37672221a6fb2b94fb001ae0c04101b239e4b1eb6c8046d3c4485da005
4
+ data.tar.gz: 928f89e4c0bfa3c5cb925b64ae437caf73aed141f704fafec4464da49d9a5a09
5
5
  SHA512:
6
- metadata.gz: ff3de870bcc72b16398b83be65bc2fb363b5274f17138fdd63532b93b34a38c1ed5959514642c16fad12a31c1040ed63039f614ec24e24281076049869d853a6
7
- data.tar.gz: fe52ff22910b5e3d701de07cc26c60036c67ab735a2945cde9a0c2f76739b1a3c13062eaa4b6dcab7306a3968906914db707fac023ade714783e69d006fb4df3
6
+ metadata.gz: 07b02e0de1972b2ce1fba69415e2719695eebd0ab69791f34270873667916088a87d214824520b6a26ddf21b6af7dadff2defa69c712f79e1258ac3d6a5e4dcf
7
+ data.tar.gz: 6e71fa92203f7ad24a9c5e581b3c57789c479cfd94651040a9078bf075358ddc53256dad73ce4453b14f282fdad5f895bd8bf124167386c4bf50ef0b9f221aac
@@ -24,6 +24,14 @@ module ForestLiana
24
24
  @optional_includes = []
25
25
  end
26
26
 
27
+ # A search predicate can only name an association the eager load actually joins;
28
+ # analyze_associations drops the rest, leaving their table out of the FROM clause.
29
+ def searchable_includes(resource)
30
+ polymorphic, preload_loads = analyze_associations(resource)
31
+
32
+ (@includes - polymorphic - preload_loads).map(&:to_sym)
33
+ end
34
+
27
35
  def optimize_record_loading(resource, records, force_preload = true)
28
36
  polymorphic, preload_loads = analyze_associations(resource)
29
37
  result = records.eager_load(@includes.uniq - preload_loads - polymorphic - @optional_includes)
@@ -14,8 +14,7 @@ module ForestLiana
14
14
  @field_names_requested = field_names_requested
15
15
  @collection = get_collection(@collection_name)
16
16
  compute_includes()
17
- includes_symbols = @includes.map { |include| include.to_sym }
18
- @search_query_builder = SearchQueryBuilder.new(@params, includes_symbols, @collection, forest_user)
17
+ @search_query_builder = SearchQueryBuilder.new(@params, searchable_includes(model_association), @collection, forest_user)
19
18
 
20
19
  prepare_query()
21
20
  end
@@ -58,6 +57,12 @@ module ForestLiana
58
57
  @records.limit(limit).offset(offset)
59
58
  end
60
59
 
60
+ def includes_for_serialization
61
+ return super if @field_names_requested.empty?
62
+
63
+ super & @field_names_requested.map(&:to_s)
64
+ end
65
+
61
66
  private
62
67
 
63
68
  def compute_includes
@@ -76,7 +81,7 @@ module ForestLiana
76
81
  inclusion = SchemaUtils.model_included?(association.klass)
77
82
  end
78
83
 
79
- if @field_names_requested.any?
84
+ if @field_names_requested.any? && !search_extended?
80
85
  inclusion && @field_names_requested.include?(association.name)
81
86
  else
82
87
  inclusion
@@ -84,6 +89,12 @@ module ForestLiana
84
89
  end.map(&:name)
85
90
  end
86
91
 
92
+ # Extended search reaches the associated tables, so its footprint must not depend
93
+ # on the projection: a count carries none, and would otherwise search more than the list.
94
+ def search_extended?
95
+ @params['searchExtended'].to_i == 1
96
+ end
97
+
87
98
  def field_names_requested
88
99
  fields = @params.dig(:fields, @collection_name)
89
100
  Array(fields&.split(',')).map(&:to_sym)
@@ -6,14 +6,14 @@ module ForestLiana
6
6
  @resource = resource
7
7
  @params = params
8
8
  @user = forest_user
9
- @count_needs_includes = false
9
+ @count_needs_includes = !@params[:search].nil?
10
10
  @collection_name = ForestLiana.name_for(@resource)
11
11
  @collection = get_collection(@collection_name)
12
12
  @fields_to_serialize = get_fields_to_serialize
13
13
  @field_names_requested = field_names_requested
14
14
  @segment = get_segment
15
15
  compute_includes
16
- @search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection, @user)
16
+ @search_query_builder = SearchQueryBuilder.new(@params, searchable_includes(@resource), @collection, @user)
17
17
 
18
18
  prepare_query
19
19
  @base_records_for_batch = @records
@@ -177,8 +177,6 @@ module ForestLiana
177
177
  end
178
178
  end
179
179
 
180
- @count_needs_includes = true if @params[:search]
181
-
182
180
  associations.uniq
183
181
  end
184
182
 
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.20.11"
2
+ VERSION = "9.20.12"
3
3
  end
@@ -130,3 +130,246 @@ describe 'Requesting Owner', :type => :request do
130
130
  end
131
131
  end
132
132
  end
133
+
134
+ describe 'Requesting Tree count with extended search', :type => :request do
135
+ let(:scope_filters) { { 'scopes' => {}, 'team' => { 'id' => '1', 'name' => 'Operations' } } }
136
+
137
+ # NOTICE: `Kiwi Tree` matches the search on its own column, `Lemon Tree` matches
138
+ # only through `owner.name`, and `Apple Tree` matches nothing. Extended
139
+ # search must therefore return 2 records, plain search 1.
140
+ before(:each) do
141
+ Tree.destroy_all
142
+ Location.destroy_all
143
+ Island.destroy_all
144
+ User.destroy_all
145
+
146
+ michel = User.create(name: 'Michel')
147
+ kiwi_grower = User.create(name: 'Kiwi Grower')
148
+ @island = Island.create(name: 'Home Island')
149
+
150
+ Tree.create(name: 'Kiwi Tree', owner: michel, cutter: michel, island: @island)
151
+ Tree.create(name: 'Lemon Tree', owner: kiwi_grower, cutter: michel, island: @island)
152
+ Tree.create(name: 'Apple Tree', owner: michel, cutter: michel, island: @island)
153
+
154
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
155
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
156
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
157
+
158
+ allow_any_instance_of(ForestLiana::Ability).to receive(:forest_authorize!) { true }
159
+
160
+ allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(scope_filters)
161
+ end
162
+
163
+ after(:each) do
164
+ Tree.destroy_all
165
+ Location.destroy_all
166
+ Island.destroy_all
167
+ User.destroy_all
168
+ end
169
+
170
+ token = JWT.encode({
171
+ id: 38,
172
+ email: 'michael.kelso@that70.show',
173
+ first_name: 'Michael',
174
+ last_name: 'Kelso',
175
+ team: 'Operations',
176
+ rendering_id: 16,
177
+ exp: Time.now.to_i + 2.weeks.to_i,
178
+ permission_level: 'admin'
179
+ }, ForestLiana.auth_secret, 'HS256')
180
+
181
+ headers = {
182
+ 'Accept' => 'application/json',
183
+ 'Content-Type' => 'application/json',
184
+ 'Authorization' => "Bearer #{token}"
185
+ }
186
+
187
+ def count_returned
188
+ JSON.parse(response.body)['count']
189
+ end
190
+
191
+ def records_returned
192
+ JSON.parse(response.body)['data'].size
193
+ end
194
+
195
+ describe 'count' do
196
+ let(:projection) { { 'Tree' => 'id,name,owner' } }
197
+ let(:params) {
198
+ {
199
+ page: { 'number' => '1', 'size' => '10' },
200
+ search: 'kiwi',
201
+ searchExtended: '1',
202
+ timezone: 'Europe/Paris'
203
+ }
204
+ }
205
+
206
+ describe 'when the request carries no fields' do
207
+ it 'responds 200 with the number of records the list returns for the same search' do
208
+ get '/forest/Tree', params: params, headers: headers
209
+ listed = records_returned
210
+
211
+ get '/forest/Tree/count', params: params, headers: headers
212
+
213
+ expect(response.status).to eq(200)
214
+ expect(count_returned).to eq(2)
215
+ expect(count_returned).to eq(listed)
216
+ end
217
+ end
218
+
219
+ describe 'when the request carries fields' do
220
+ it 'responds 200 with the number of records the list returns for the same search' do
221
+ get '/forest/Tree', params: params.merge(fields: projection), headers: headers
222
+ listed = records_returned
223
+
224
+ get '/forest/Tree/count', params: params.merge(fields: projection), headers: headers
225
+
226
+ expect(response.status).to eq(200)
227
+ expect(count_returned).to eq(2)
228
+ expect(count_returned).to eq(listed)
229
+ end
230
+ end
231
+
232
+ describe 'when the same count is requested twice' do
233
+ it 'returns the same count both times' do
234
+ get '/forest/Tree/count', params: params, headers: headers
235
+ first_count = count_returned
236
+
237
+ get '/forest/Tree/count', params: params, headers: headers
238
+
239
+ expect(response.status).to eq(200)
240
+ expect(count_returned).to eq(2)
241
+ expect(count_returned).to eq(first_count)
242
+ end
243
+ end
244
+
245
+ describe 'when extended search is off' do
246
+ it 'counts only the records matching on their own columns' do
247
+ get '/forest/Tree', params: params.merge(searchExtended: '0'), headers: headers
248
+ listed = records_returned
249
+
250
+ get '/forest/Tree/count', params: params.merge(searchExtended: '0'), headers: headers
251
+
252
+ expect(response.status).to eq(200)
253
+ expect(count_returned).to eq(1)
254
+ expect(count_returned).to eq(listed)
255
+ end
256
+ end
257
+
258
+ describe 'when searchExtended is absent from the request' do
259
+ it 'counts only the records matching on their own columns' do
260
+ get '/forest/Tree/count', params: params.except(:searchExtended), headers: headers
261
+
262
+ expect(response.status).to eq(200)
263
+ expect(count_returned).to eq(1)
264
+ end
265
+ end
266
+
267
+ describe 'when the search matches no record at all' do
268
+ it 'responds 200 with a count of zero' do
269
+ get '/forest/Tree', params: params.merge(search: 'sequoia'), headers: headers
270
+ listed = records_returned
271
+
272
+ get '/forest/Tree/count', params: params.merge(search: 'sequoia'), headers: headers
273
+
274
+ expect(response.status).to eq(200)
275
+ expect(count_returned).to eq(0)
276
+ expect(count_returned).to eq(listed)
277
+ end
278
+ end
279
+
280
+ describe 'when the search is an empty string' do
281
+ it 'counts every record' do
282
+ get '/forest/Tree/count', params: params.merge(search: ''), headers: headers
283
+
284
+ expect(response.status).to eq(200)
285
+ expect(count_returned).to eq(3)
286
+ end
287
+ end
288
+
289
+ describe 'when the request carries no search at all' do
290
+ it 'counts every record' do
291
+ get '/forest/Tree/count', params: params.except(:search), headers: headers
292
+
293
+ expect(response.status).to eq(200)
294
+ expect(count_returned).to eq(3)
295
+ end
296
+ end
297
+ end
298
+
299
+ describe 'count on relationships' do
300
+ let(:projection) { { 'Tree' => 'id,name' } }
301
+ let(:params) {
302
+ {
303
+ page: { 'number' => '1', 'size' => '10' },
304
+ search: 'kiwi',
305
+ searchExtended: '1',
306
+ timezone: 'Europe/Paris'
307
+ }
308
+ }
309
+
310
+ describe 'when the count carries no fields and the list carries the projection' do
311
+ it 'counts the number of records the related list returns' do
312
+ get "/forest/Island/#{@island.id}/relationships/trees",
313
+ params: params.merge(fields: projection), headers: headers
314
+ listed = records_returned
315
+
316
+ get "/forest/Island/#{@island.id}/relationships/trees/count", params: params, headers: headers
317
+
318
+ expect(response.status).to eq(200)
319
+ expect(count_returned).to eq(2)
320
+ expect(count_returned).to eq(listed)
321
+ end
322
+ end
323
+
324
+ describe 'when both the count and the list carry the projection' do
325
+ it 'counts the number of records the related list returns' do
326
+ get "/forest/Island/#{@island.id}/relationships/trees",
327
+ params: params.merge(fields: projection), headers: headers
328
+ listed = records_returned
329
+
330
+ get "/forest/Island/#{@island.id}/relationships/trees/count",
331
+ params: params.merge(fields: projection), headers: headers
332
+
333
+ expect(response.status).to eq(200)
334
+ expect(count_returned).to eq(2)
335
+ expect(count_returned).to eq(listed)
336
+ end
337
+ end
338
+
339
+ describe 'when extended search is off' do
340
+ it 'counts only the related records matching on their own columns' do
341
+ get "/forest/Island/#{@island.id}/relationships/trees",
342
+ params: params.merge(searchExtended: '0', fields: projection), headers: headers
343
+ listed = records_returned
344
+
345
+ get "/forest/Island/#{@island.id}/relationships/trees/count",
346
+ params: params.merge(searchExtended: '0'), headers: headers
347
+
348
+ expect(response.status).to eq(200)
349
+ expect(count_returned).to eq(1)
350
+ expect(count_returned).to eq(listed)
351
+ end
352
+ end
353
+
354
+ describe 'when the related search matches no record at all' do
355
+ it 'responds 200 with a count of zero' do
356
+ get "/forest/Island/#{@island.id}/relationships/trees/count",
357
+ params: params.merge(search: 'sequoia'), headers: headers
358
+
359
+ expect(response.status).to eq(200)
360
+ expect(count_returned).to eq(0)
361
+ end
362
+ end
363
+
364
+ describe 'when the parent record has no related record' do
365
+ it 'responds 200 with a count of zero' do
366
+ empty_island = Island.create(name: 'Empty Island')
367
+
368
+ get "/forest/Island/#{empty_island.id}/relationships/trees/count", params: params, headers: headers
369
+
370
+ expect(response.status).to eq(200)
371
+ expect(count_returned).to eq(0)
372
+ end
373
+ end
374
+ end
375
+ end
@@ -114,6 +114,145 @@ module ForestLiana
114
114
  end
115
115
  end
116
116
 
117
+ describe 'when searching with extended search' do
118
+ # HashWithIndifferentAccess: the code reads @params['searchExtended'] (string),
119
+ # as the controller hands it an ActionController::Parameters.
120
+ let(:projection) { nil }
121
+ let(:searchExtended) { '1' }
122
+ let(:params) {
123
+ ActiveSupport::HashWithIndifferentAccess.new(
124
+ id: Island.first.id,
125
+ association_name: 'trees',
126
+ search: 'kiwi',
127
+ searchExtended: searchExtended,
128
+ fields: projection,
129
+ page: { size: 15, number: 1 },
130
+ timezone: 'America/Nome'
131
+ )
132
+ }
133
+
134
+ # NOTICE: `kiwi tree` matches on its own name and `coconut tree` matches only
135
+ # through `owner.name`, so extended search matches 2 of madagascar's
136
+ # trees where plain search matches 1.
137
+ before(:each) do
138
+ kiwi_grower = User.create(name: 'Kiwi Grower')
139
+ Tree.create(name: 'kiwi tree', island: Island.first)
140
+ Tree.create(name: 'coconut tree', island: Island.first, owner: kiwi_grower)
141
+ end
142
+
143
+ after(:each) do
144
+ User.destroy_all
145
+ end
146
+
147
+ describe 'when the request carries no fields' do
148
+ it 'counts the related records it returns' do
149
+ subject.perform
150
+
151
+ expect(subject.count).to eq 2
152
+ expect(subject.records.count).to eq 2
153
+ end
154
+ end
155
+
156
+ describe 'when the request carries fields' do
157
+ let(:projection) { { 'Tree' => 'id,name' } }
158
+
159
+ it 'counts the same related records as the request carrying no fields' do
160
+ subject.perform
161
+
162
+ expect(subject.count).to eq 2
163
+ expect(subject.records.count).to eq 2
164
+ end
165
+ end
166
+
167
+ describe 'when the count carries no fields and the list carries the projection' do
168
+ it 'announces the number of related records the list returns' do
169
+ listed = described_class.new(
170
+ Island,
171
+ association,
172
+ params.merge(fields: { 'Tree' => 'id,name' }),
173
+ user
174
+ )
175
+ listed.perform
176
+
177
+ expect(subject.count).to eq 2
178
+ expect(listed.records.count).to eq 2
179
+ end
180
+ end
181
+
182
+ describe 'when extended search is off' do
183
+ let(:searchExtended) { '0' }
184
+
185
+ it 'counts only the related records matching on their own columns' do
186
+ subject.perform
187
+
188
+ expect(subject.count).to eq 1
189
+ expect(subject.records.count).to eq 1
190
+ end
191
+ end
192
+
193
+ describe 'serializing the related records' do
194
+ let(:projection) { { 'Tree' => 'id,name,owner' } }
195
+
196
+ it 'serializes only the associations the projection asked for' do
197
+ expect(subject.includes_for_serialization).to eq ['owner']
198
+ end
199
+ end
200
+
201
+ describe 'serializing the related records when no projection was requested' do
202
+ it 'serializes every association the search reaches' do
203
+ expect(subject.includes_for_serialization)
204
+ .to match_array(%w[owner cutter island eponymous_island location])
205
+ end
206
+ end
207
+ end
208
+
209
+ describe 'when the related collection has an association in another database' do
210
+ let(:association) { Manufacturer.reflect_on_association(:products) }
211
+ let(:search) { 'zzz' }
212
+ let(:params) {
213
+ ActiveSupport::HashWithIndifferentAccess.new(
214
+ id: Manufacturer.first.id,
215
+ association_name: 'products',
216
+ search: search,
217
+ searchExtended: '1',
218
+ fields: { 'Product' => 'id,name' },
219
+ page: { size: 15, number: 1 },
220
+ timezone: 'America/Nome'
221
+ )
222
+ }
223
+
224
+ subject { described_class.new(Manufacturer, association, params, user) }
225
+
226
+ before(:each) do
227
+ manufacturer = Manufacturer.create!(name: 'Acme')
228
+ driver = Driver.create!(firstname: 'Zed')
229
+ Product.create!(
230
+ name: 'Widget', uri: 'https://widget.example',
231
+ manufacturer: manufacturer, driver: driver
232
+ )
233
+ end
234
+
235
+ after(:each) do
236
+ Product.destroy_all
237
+ Manufacturer.destroy_all
238
+ Driver.destroy_all
239
+ end
240
+
241
+ it 'does not search columns the eager load cannot join' do
242
+ expect(subject.count).to eq 0
243
+ expect(subject.records.count).to eq 0
244
+ end
245
+
246
+ describe 'when the search matches an association in the same database' do
247
+ let(:search) { 'acme' }
248
+
249
+ it 'still matches through that association' do
250
+ expect(subject.count).to eq 1
251
+ expect(subject.records.count).to eq 1
252
+ end
253
+ end
254
+ end
255
+
117
256
  describe 'compute_includes' do
118
257
  it 'should include has_one relation from association' do
119
258
  expect(subject.includes).to include(:location)
@@ -154,6 +293,21 @@ module ForestLiana
154
293
  expect(subject.includes).to be_empty
155
294
  end
156
295
 
296
+ it 'should not narrow the includes by the requested fields when extended search is on' do
297
+ extended_params = ActiveSupport::HashWithIndifferentAccess.new(
298
+ id: Island.first.id,
299
+ association_name: 'trees',
300
+ search: 'kiwi',
301
+ searchExtended: '1',
302
+ fields: { 'Tree' => 'owner,island' },
303
+ page: { size: 15, number: 1 },
304
+ timezone: 'America/Nome'
305
+ )
306
+ getter = described_class.new(Island, association, extended_params, user)
307
+
308
+ expect(getter.includes).to match_array(%i[owner cutter island eponymous_island location])
309
+ end
310
+
157
311
  context 'on polymorphic associations' do
158
312
  let(:base_params) do
159
313
  {
@@ -628,5 +628,111 @@ module ForestLiana
628
628
  end
629
629
  end
630
630
  end
631
+
632
+ describe 'when searching with extended search' do
633
+ let(:resource) { Tree }
634
+ let(:search) { 'skull' }
635
+ let(:searchExtended) { '1' }
636
+ let(:requested_fields) { nil }
637
+
638
+ # HashWithIndifferentAccess: the code reads @params['searchExtended'] (string),
639
+ # as the controller hands it an ActionController::Parameters.
640
+ let(:search_params) {
641
+ ActiveSupport::HashWithIndifferentAccess.new(
642
+ page: { size: pageSize, number: pageNumber },
643
+ search: search,
644
+ searchExtended: searchExtended,
645
+ fields: requested_fields,
646
+ timezone: 'Europe/Paris',
647
+ )
648
+ }
649
+
650
+ # NOTICE: the count route builds a getter and calls #count on it without ever
651
+ # calling #perform, so the list getter has to be a separate instance —
652
+ # #perform rewrites @records with the eager loads and would hide the defect.
653
+ let(:getter) { described_class.new(resource, search_params, user) }
654
+ let(:list_getter) { described_class.new(resource, search_params, user) }
655
+
656
+ # NOTICE: `Lemon Tree` and `Ginger Tree` both grow on the island `Skull`, and no
657
+ # tree carries `skull` in its own name — so extended search matches 2
658
+ # records where plain search matches none.
659
+ describe 'when no fields are requested' do
660
+ it 'counts the records the list returns instead of failing on the un-joined association' do
661
+ list_getter.perform
662
+
663
+ expect(getter.count).to eq 2
664
+ expect(list_getter.records.count).to eq 2
665
+ end
666
+
667
+ it 'keeps every one-association in the include set when no fields are requested' do
668
+ expect(getter.includes).to contain_exactly(:owner, :cutter, :island, :eponymous_island, :location)
669
+ end
670
+ end
671
+
672
+ describe 'when fields are requested' do
673
+ let(:requested_fields) { { 'Tree' => 'id,name' } }
674
+
675
+ it 'counts the same records as the request carrying no fields' do
676
+ list_getter.perform
677
+
678
+ expect(getter.count).to eq 2
679
+ expect(list_getter.records.count).to eq 2
680
+ end
681
+
682
+ it 'keeps the same include set when fields are requested' do
683
+ expect(getter.includes).to contain_exactly(:owner, :cutter, :island, :eponymous_island, :location)
684
+ end
685
+ end
686
+
687
+ describe 'when extended search is off' do
688
+ let(:searchExtended) { '0' }
689
+
690
+ it 'matches on the own columns only and counts none of the associated records' do
691
+ list_getter.perform
692
+
693
+ expect(getter.count).to eq 0
694
+ expect(list_getter.records.count).to eq 0
695
+ expect(getter.includes).to eq []
696
+ end
697
+ end
698
+
699
+ describe 'when the search is nil' do
700
+ let(:search) { nil }
701
+
702
+ it 'counts every record' do
703
+ list_getter.perform
704
+
705
+ expect(getter.count).to eq 5
706
+ expect(list_getter.records.count).to eq 5
707
+ end
708
+ end
709
+
710
+ # NOTICE: Island's one-associations are `location` and the instance-dependent
711
+ # `eponymous_tree`. Nothing else joins `trees`, so a predicate naming it
712
+ # has no FROM entry — the shape `Tree` hides, since `eponymous_island`
713
+ # resolves against the `isle` join `island` already contributes.
714
+ describe 'when a one-association carries an instance-dependent scope' do
715
+ let(:resource) { Island }
716
+ let(:search) { 'skull' }
717
+
718
+ it 'searches the own columns without failing on the un-joined association' do
719
+ list_getter.perform
720
+
721
+ expect(getter.count).to eq 1
722
+ expect(list_getter.records.count).to eq 1
723
+ end
724
+
725
+ describe 'when the search matches through an association the eager load does join' do
726
+ let(:search) { '12345' }
727
+
728
+ it 'still matches through that association' do
729
+ list_getter.perform
730
+
731
+ expect(getter.count).to eq 1
732
+ expect(list_getter.records.count).to eq 1
733
+ end
734
+ end
735
+ end
736
+ end
631
737
  end
632
738
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.20.11
4
+ version: 9.20.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails