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 +4 -4
- data/app/services/forest_liana/base_getter.rb +8 -0
- data/app/services/forest_liana/has_many_getter.rb +14 -3
- data/app/services/forest_liana/resources_getter.rb +2 -4
- data/lib/forest_liana/version.rb +1 -1
- data/spec/requests/count_spec.rb +243 -0
- data/spec/services/forest_liana/has_many_getter_spec.rb +154 -0
- data/spec/services/forest_liana/resources_getter_spec.rb +106 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 113a6c37672221a6fb2b94fb001ae0c04101b239e4b1eb6c8046d3c4485da005
|
|
4
|
+
data.tar.gz: 928f89e4c0bfa3c5cb925b64ae437caf73aed141f704fafec4464da49d9a5a09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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 =
|
|
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, @
|
|
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
|
|
data/lib/forest_liana/version.rb
CHANGED
data/spec/requests/count_spec.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
11
|
+
date: 2026-08-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|