forest_liana 9.3.13 → 9.3.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/services/forest_liana/base_getter.rb +22 -29
- data/app/services/forest_liana/filters_parser.rb +7 -11
- data/app/services/forest_liana/resources_getter.rb +97 -116
- data/lib/forest_liana/version.rb +1 -1
- data/spec/services/forest_liana/filters_parser_spec.rb +1 -1
- data/spec/services/forest_liana/resources_getter_spec.rb +25 -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: 53102b046de3c8137e31de9c1cdd7581d860e497ef870f0fee0c08e4faa34c3f
|
4
|
+
data.tar.gz: 1eaf3d98e252b8683fb6b2398058dd86b269b45df5f6644c488d2aba5c110206
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ccf9b6efd785f8ffcc39f0e0514ba73c408b98ae5a842695279b38127fb17cc8e5b555391596c5e2132933cb981a9916d641ff5cb7fb58481cbe5928b1a5d1c
|
7
|
+
data.tar.gz: 58c7b9630f16d8cbb00fa4c058a6002c635f3f07c0c3a0183989b9acbeda16667864a3e183fb1557faf6a38c1b6062056f720364722148fbfcd8facd6e4e4a3a
|
@@ -5,23 +5,14 @@ module ForestLiana
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def get_resource
|
8
|
-
|
9
|
-
.include? :really_destroyed?
|
10
|
-
|
11
|
-
# NOTICE: Do not unscope with the paranoia gem to prevent the retrieval
|
12
|
-
# of deleted records.
|
13
|
-
use_act_as_paranoid ? @resource : @resource.unscoped
|
8
|
+
@resource.instance_methods.include?(:really_destroyed?) ? @resource : @resource.unscoped
|
14
9
|
end
|
15
10
|
|
16
11
|
def includes_for_serialization
|
17
|
-
includes_initial = @includes
|
18
12
|
includes_for_smart_belongs_to = @collection.fields_smart_belongs_to.map { |field| field[:field] }
|
13
|
+
includes_for_smart_belongs_to &= @field_names_requested if @field_names_requested
|
19
14
|
|
20
|
-
|
21
|
-
includes_for_smart_belongs_to = includes_for_smart_belongs_to & @field_names_requested
|
22
|
-
end
|
23
|
-
|
24
|
-
includes_initial.concat(includes_for_smart_belongs_to).map(&:to_s)
|
15
|
+
@includes.concat(includes_for_smart_belongs_to).map(&:to_s)
|
25
16
|
end
|
26
17
|
|
27
18
|
private
|
@@ -31,34 +22,36 @@ module ForestLiana
|
|
31
22
|
end
|
32
23
|
|
33
24
|
def optimize_record_loading(resource, records)
|
34
|
-
|
25
|
+
polymorphic, preload_loads = analyze_associations(resource)
|
26
|
+
result = records.eager_load(@includes.uniq - preload_loads - polymorphic)
|
35
27
|
|
28
|
+
result = result.preload(preload_loads) if Rails::VERSION::MAJOR >= 7
|
29
|
+
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def analyze_associations(resource)
|
36
34
|
polymorphic = []
|
37
|
-
preload_loads = @includes.select do |name|
|
35
|
+
preload_loads = @includes.uniq.select do |name|
|
38
36
|
association = resource.reflect_on_association(name)
|
39
37
|
if SchemaUtils.polymorphic?(association)
|
40
38
|
polymorphic << association.name
|
41
39
|
false
|
42
40
|
else
|
43
|
-
|
44
|
-
targetModelDatabase = targetModelConnection.current_database if targetModelConnection.respond_to? :current_database
|
45
|
-
resourceConnection = resource.connection
|
46
|
-
resourceDatabase = resourceConnection.current_database if resourceConnection.respond_to? :current_database
|
47
|
-
|
48
|
-
targetModelDatabase != resourceDatabase
|
41
|
+
separate_database?(resource, association)
|
49
42
|
end
|
50
|
-
end + instance_dependent_associations
|
43
|
+
end + instance_dependent_associations(resource)
|
51
44
|
|
52
|
-
|
45
|
+
[polymorphic, preload_loads]
|
46
|
+
end
|
53
47
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
48
|
+
def separate_database?(resource, association)
|
49
|
+
target_model_connection = association.klass.connection
|
50
|
+
target_model_database = target_model_connection.current_database if target_model_connection.respond_to? :current_database
|
51
|
+
resource_connection = resource.connection
|
52
|
+
resource_database = resource_connection.current_database if resource_connection.respond_to? :current_database
|
60
53
|
|
61
|
-
|
54
|
+
target_model_database != resource_database
|
62
55
|
end
|
63
56
|
|
64
57
|
def instance_dependent_associations(resource)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module ForestLiana
|
2
2
|
class FiltersParser
|
3
|
-
AGGREGATOR_OPERATOR = %w(and or)
|
3
|
+
AGGREGATOR_OPERATOR = %w(and or).freeze
|
4
4
|
|
5
5
|
def initialize(filters, resource, timezone, params = nil)
|
6
6
|
@filters = filters
|
@@ -13,18 +13,14 @@ module ForestLiana
|
|
13
13
|
def apply_filters
|
14
14
|
return @resource unless @filters
|
15
15
|
|
16
|
-
|
17
|
-
return @resource unless
|
16
|
+
where_clause = parse_aggregation(@filters)
|
17
|
+
return @resource unless where_clause
|
18
18
|
|
19
19
|
@joins.each do |join|
|
20
|
-
|
21
|
-
current_resource.include(ArelHelpers::Aliases)
|
22
|
-
current_resource.aliased_as(join.name) do |aliased_resource|
|
23
|
-
@resource = @resource.joins(ArelHelpers.join_association(@resource, join.name, Arel::Nodes::OuterJoin, aliases: [aliased_resource]))
|
24
|
-
end
|
20
|
+
@resource = @resource.eager_load(join.name)
|
25
21
|
end
|
26
22
|
|
27
|
-
@resource.where(
|
23
|
+
@resource.where(where_clause)
|
28
24
|
end
|
29
25
|
|
30
26
|
def parse_aggregation(node)
|
@@ -170,8 +166,8 @@ module ForestLiana
|
|
170
166
|
current_resource = @resource.reflect_on_association(field.split(':').first.to_sym)&.klass
|
171
167
|
raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found") unless current_resource
|
172
168
|
|
173
|
-
|
174
|
-
quoted_table_name =
|
169
|
+
get_association_name_for_condition(field)
|
170
|
+
quoted_table_name = current_resource.table_name
|
175
171
|
field_name = field.split(':')[1]
|
176
172
|
else
|
177
173
|
quoted_table_name = @resource.quoted_table_name
|
@@ -1,68 +1,32 @@
|
|
1
1
|
module ForestLiana
|
2
2
|
class ResourcesGetter < BaseGetter
|
3
|
-
attr_reader :search_query_builder
|
4
|
-
attr_reader :includes
|
5
|
-
attr_reader :records_count
|
3
|
+
attr_reader :search_query_builder, :includes, :records_count
|
6
4
|
|
7
5
|
def initialize(resource, params, forest_user)
|
8
6
|
@resource = resource
|
9
7
|
@params = params
|
8
|
+
@user = forest_user
|
10
9
|
@count_needs_includes = false
|
11
10
|
@collection_name = ForestLiana.name_for(@resource)
|
12
11
|
@collection = get_collection(@collection_name)
|
13
12
|
@fields_to_serialize = get_fields_to_serialize
|
14
13
|
@field_names_requested = field_names_requested
|
15
|
-
get_segment
|
14
|
+
@segment = get_segment
|
16
15
|
compute_includes
|
17
|
-
@
|
18
|
-
@search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection, forest_user)
|
16
|
+
@search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection, @user)
|
19
17
|
|
20
18
|
prepare_query
|
21
19
|
end
|
22
20
|
|
23
21
|
def self.get_ids_from_request(params, user)
|
24
22
|
attributes = params.dig('data', 'attributes')
|
25
|
-
|
26
|
-
is_select_all_records_query = has_body_attributes && attributes[:all_records] == true
|
27
|
-
|
28
|
-
# NOTICE: If it is not a "select all records" query and it receives a list of ID, return list of ID.
|
29
|
-
return attributes[:ids] if (!is_select_all_records_query && attributes[:ids])
|
30
|
-
|
31
|
-
# NOTICE: If it is a "select all records" we have to perform query to build ID list.
|
32
|
-
ids = Array.new
|
33
|
-
|
34
|
-
# NOTICE: Merging all_records_subset_query into attributes preserves filters in HasManyGetter and ResourcesGetter.
|
35
|
-
attributes = attributes.merge(attributes[:all_records_subset_query].dup.to_unsafe_h)
|
36
|
-
|
37
|
-
# NOTICE: Initialize actual resources getter (could either a HasManyGetter or a ResourcesGetter).
|
38
|
-
is_related_data = attributes[:parent_collection_id] &&
|
39
|
-
attributes[:parent_collection_name] &&
|
40
|
-
attributes[:parent_association_name]
|
41
|
-
if is_related_data
|
42
|
-
parent_collection_name = attributes[:parent_collection_name]
|
43
|
-
parent_model = ForestLiana::SchemaUtils.find_model_from_collection_name(parent_collection_name)
|
44
|
-
model = parent_model.reflect_on_association(attributes[:parent_association_name].try(:to_sym))
|
45
|
-
resources_getter = ForestLiana::HasManyGetter.new(parent_model, model, attributes.merge({
|
46
|
-
collection: parent_collection_name,
|
47
|
-
id: attributes[:parent_collection_id],
|
48
|
-
association_name: attributes[:parent_association_name],
|
49
|
-
}), user)
|
50
|
-
else
|
51
|
-
collection_name = attributes[:collection_name]
|
52
|
-
model = ForestLiana::SchemaUtils.find_model_from_collection_name(collection_name)
|
53
|
-
resources_getter = ForestLiana::ResourcesGetter.new(model, attributes, user)
|
54
|
-
end
|
23
|
+
return attributes[:ids] if attributes&.fetch(:all_records, false) == false && attributes[:ids]
|
55
24
|
|
56
|
-
|
57
|
-
resources_getter
|
58
|
-
|
59
|
-
end
|
60
|
-
|
61
|
-
# NOTICE: remove excluded IDs.
|
62
|
-
ids_excluded = (attributes[:all_records_ids_excluded]).map { |id_excluded| id_excluded.to_s }
|
63
|
-
return ids.select { |id| !ids_excluded.include? id.to_s } if (ids_excluded && ids_excluded.any?)
|
25
|
+
attributes = merge_subset_query(attributes)
|
26
|
+
resources_getter = initialize_resources_getter(attributes, user)
|
27
|
+
ids = fetch_ids(resources_getter)
|
64
28
|
|
65
|
-
|
29
|
+
filter_excluded_ids(ids, attributes[:all_records_ids_excluded])
|
66
30
|
end
|
67
31
|
|
68
32
|
def perform
|
@@ -70,9 +34,7 @@ module ForestLiana
|
|
70
34
|
end
|
71
35
|
|
72
36
|
def count
|
73
|
-
|
74
|
-
# filters on associations.
|
75
|
-
@records_count = @count_needs_includes ? optimize_record_loading(@resource, @records).count : @records.count
|
37
|
+
@records_count = @count_needs_includes ? optimized_count : @records.count
|
76
38
|
end
|
77
39
|
|
78
40
|
def query_for_batch
|
@@ -91,12 +53,12 @@ module ForestLiana
|
|
91
53
|
|
92
54
|
if @collection && @collection.search_fields
|
93
55
|
includes_for_smart_search = @collection.search_fields
|
94
|
-
|
95
|
-
|
56
|
+
.select { |field| field.include? '.' }
|
57
|
+
.map { |field| field.split('.').first.to_sym }
|
96
58
|
|
97
59
|
includes_has_many = SchemaUtils.many_associations(@resource)
|
98
|
-
|
99
|
-
|
60
|
+
.select { |association| SchemaUtils.model_included?(association.klass) }
|
61
|
+
.map(&:name)
|
100
62
|
|
101
63
|
includes_for_smart_search = includes_for_smart_search & includes_has_many
|
102
64
|
end
|
@@ -115,107 +77,126 @@ module ForestLiana
|
|
115
77
|
private
|
116
78
|
|
117
79
|
def get_fields_to_serialize
|
118
|
-
|
119
|
-
@params[:fields][@collection_name].split(',').map { |name| name.to_sym }
|
120
|
-
else
|
121
|
-
[]
|
122
|
-
end
|
80
|
+
@params.dig(:fields, @collection_name)&.split(',')&.map(&:to_sym) || []
|
123
81
|
end
|
124
82
|
|
125
83
|
def get_segment
|
126
|
-
if @params[:segment]
|
127
|
-
@segment = @collection.segments.find do |segment|
|
128
|
-
segment.name == @params[:segment]
|
129
|
-
end
|
130
|
-
end
|
131
|
-
@segment ||= nil
|
84
|
+
@collection.segments.find { |segment| segment.name == @params[:segment] } if @params[:segment]
|
132
85
|
end
|
133
86
|
|
134
87
|
def field_names_requested
|
135
88
|
return nil unless @params[:fields] && @params[:fields][@collection_name]
|
136
89
|
|
137
|
-
associations_for_query =
|
90
|
+
associations_for_query = extract_associations_from_filter
|
91
|
+
associations_for_query << @params[:sort].split('.').first.to_sym if @params[:sort]&.include?('.')
|
92
|
+
@fields_to_serialize | associations_for_query
|
93
|
+
end
|
138
94
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
end
|
95
|
+
def extract_associations_from_filter
|
96
|
+
associations = []
|
97
|
+
@params[:filter]&.each do |field, _|
|
98
|
+
if field.include?(':')
|
99
|
+
associations << field.split(':').first.to_sym
|
100
|
+
@count_needs_includes = true
|
146
101
|
end
|
147
102
|
end
|
148
|
-
|
149
103
|
@count_needs_includes = true if @params[:search]
|
150
104
|
|
151
|
-
|
152
|
-
|
105
|
+
associations
|
106
|
+
end
|
107
|
+
|
108
|
+
def prepare_query
|
109
|
+
@records = get_resource
|
110
|
+
|
111
|
+
if @segment
|
112
|
+
@records = apply_segment(@records)
|
153
113
|
end
|
154
114
|
|
155
|
-
|
115
|
+
apply_live_query_segment if @params[:segmentQuery]
|
116
|
+
@records = search_query
|
156
117
|
end
|
157
118
|
|
158
|
-
def
|
159
|
-
|
119
|
+
def optimized_count
|
120
|
+
optimize_record_loading(@resource, @records).count
|
160
121
|
end
|
161
122
|
|
162
|
-
def
|
163
|
-
@
|
123
|
+
def apply_segment(records)
|
124
|
+
return records.send(@segment.scope) if @segment.scope
|
125
|
+
return records.where(@segment.where.call) if @segment.where
|
126
|
+
|
127
|
+
records
|
128
|
+
end
|
164
129
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
130
|
+
def apply_live_query_segment
|
131
|
+
LiveQueryChecker.new(@params[:segmentQuery], 'Live Query Segment').validate
|
132
|
+
|
133
|
+
begin
|
134
|
+
segment_query = @params[:segmentQuery].gsub(/\;\s*$/, '')
|
135
|
+
@records = @records.where(
|
136
|
+
"#{@resource.table_name}.#{@resource.primary_key} IN (SELECT id FROM (#{segment_query}) as ids)"
|
137
|
+
)
|
138
|
+
rescue => error
|
139
|
+
handle_live_query_error(error)
|
169
140
|
end
|
141
|
+
end
|
170
142
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
143
|
+
def handle_live_query_error(error)
|
144
|
+
error_message = "Live Query Segment: #{error.message}"
|
145
|
+
FOREST_REPORTER.report error
|
146
|
+
FOREST_LOGGER.error(error_message)
|
147
|
+
raise ForestLiana::Errors::LiveQueryError.new(error_message)
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.merge_subset_query(attributes)
|
151
|
+
attributes.merge(attributes[:all_records_subset_query].dup.to_unsafe_h)
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.initialize_resources_getter(attributes, user)
|
155
|
+
if related_data?(attributes)
|
156
|
+
HasManyGetter.new(*related_data_params(attributes, user))
|
157
|
+
else
|
158
|
+
ResourcesGetter.new(SchemaUtils.find_model_from_collection_name(attributes[:collection_name]), attributes, user)
|
186
159
|
end
|
160
|
+
end
|
187
161
|
|
188
|
-
|
162
|
+
def self.related_data?(attributes)
|
163
|
+
attributes[:parent_collection_id] && attributes[:parent_collection_name] && attributes[:parent_association_name]
|
189
164
|
end
|
190
165
|
|
191
|
-
def
|
192
|
-
|
166
|
+
def self.related_data_params(attributes, user)
|
167
|
+
parent_model = SchemaUtils.find_model_from_collection_name(attributes[:parent_collection_name])
|
168
|
+
model = parent_model.reflect_on_association(attributes[:parent_association_name].to_sym)
|
169
|
+
[parent_model, model, attributes.merge(collection: attributes[:parent_collection_name]), user]
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.fetch_ids(resources_getter)
|
173
|
+
ids = []
|
174
|
+
resources_getter.query_for_batch.find_in_batches { |records| ids += records.map(&:id) }
|
175
|
+
|
176
|
+
ids
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.filter_excluded_ids(ids, ids_excluded)
|
180
|
+
ids_excluded ? ids.reject { |id| ids_excluded.map(&:to_s).include?(id.to_s) } : ids
|
181
|
+
end
|
182
|
+
|
183
|
+
def search_query
|
184
|
+
@search_query_builder.perform(@records)
|
193
185
|
end
|
194
186
|
|
195
187
|
def offset
|
196
188
|
return 0 unless pagination?
|
197
189
|
|
198
|
-
number = @params
|
199
|
-
|
200
|
-
(number.to_i - 1) * limit
|
201
|
-
else
|
202
|
-
0
|
203
|
-
end
|
190
|
+
number = @params.dig(:page, :number)
|
191
|
+
number.to_i.positive? ? (number.to_i - 1) * limit : 0
|
204
192
|
end
|
205
193
|
|
206
194
|
def limit
|
207
|
-
|
208
|
-
|
209
|
-
if @params[:page][:size]
|
210
|
-
@params[:page][:size].to_i
|
211
|
-
else
|
212
|
-
10
|
213
|
-
end
|
195
|
+
@params.dig(:page, :size)&.to_i || 10
|
214
196
|
end
|
215
197
|
|
216
198
|
def pagination?
|
217
|
-
@params[:page]
|
199
|
+
@params[:page]&.dig(:number)
|
218
200
|
end
|
219
|
-
|
220
201
|
end
|
221
202
|
end
|
data/lib/forest_liana/version.rb
CHANGED
@@ -421,7 +421,7 @@ module ForestLiana
|
|
421
421
|
context 'on belongs to field' do
|
422
422
|
context 'existing field' do
|
423
423
|
let(:field_name) { 'trees:age' }
|
424
|
-
it { expect(result).to eq "
|
424
|
+
it { expect(result).to eq "trees.\"age\""}
|
425
425
|
end
|
426
426
|
context 'not existing field' do
|
427
427
|
let(:field_name) { 'hero:age' }
|
@@ -248,6 +248,31 @@ module ForestLiana
|
|
248
248
|
end
|
249
249
|
end
|
250
250
|
|
251
|
+
context 'when fields is given' do
|
252
|
+
let(:resource) { Island }
|
253
|
+
let(:filters) { {
|
254
|
+
field: 'location:id',
|
255
|
+
operator: 'equal',
|
256
|
+
value: 1,
|
257
|
+
}.to_json }
|
258
|
+
|
259
|
+
it 'should get only the expected records' do
|
260
|
+
getter.perform
|
261
|
+
records = getter.records
|
262
|
+
count = getter.count
|
263
|
+
|
264
|
+
expect(records.count).to eq 1
|
265
|
+
expect(count).to eq 1
|
266
|
+
expect(records.map(&:id)).to eq [1]
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'should include associated table only once' do
|
270
|
+
sql_query = getter.perform.to_sql
|
271
|
+
location_includes_count = sql_query.scan('LEFT OUTER JOIN "locations"').count
|
272
|
+
expect(location_includes_count).to eq(1)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
251
276
|
describe 'when getting instance dependent associations' do
|
252
277
|
let(:resource) { Island }
|
253
278
|
let(:fields) { { 'Island' => 'id,eponymous_tree', 'eponymous_tree' => 'id,name'} }
|
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.3.
|
4
|
+
version: 9.3.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sandro Munda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|