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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71e166bf8607e91299070c9f6158460e2e997488bbc7162bc09e0c7882f0e5fa
4
- data.tar.gz: 6bf47b9c89e0ac3bf20e13b03a72f36a958e8689831f4be4ebe41af505ae960f
3
+ metadata.gz: 53102b046de3c8137e31de9c1cdd7581d860e497ef870f0fee0c08e4faa34c3f
4
+ data.tar.gz: 1eaf3d98e252b8683fb6b2398058dd86b269b45df5f6644c488d2aba5c110206
5
5
  SHA512:
6
- metadata.gz: bccaf2fb14aa6dcba206670cc13ec7356bcd342ccc4a1b89ab05f726f46e484ecf7a44fc3c0f58bf0fc8d784f3bf168b1a1d0fa9ce94224b422329668ab7967c
7
- data.tar.gz: f79f05328165367a3f401896a221e1e45b5f2175417cd51e1704cca69c288c1429ad23cc257621d9f099350e328e3f0d4ebd5801782e491ab7ac43e42df6596a
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
- use_act_as_paranoid = @resource.instance_methods
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
- if @field_names_requested
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
- instance_dependent_associations = instance_dependent_associations(resource)
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
- targetModelConnection = association.klass.connection
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
- result = records.eager_load(@includes - preload_loads - polymorphic)
45
+ [polymorphic, preload_loads]
46
+ end
53
47
 
54
- # Rails 7 can mix `eager_load` and `preload` in the same scope
55
- # Rails 6 cannot mix `eager_load` and `preload` in the same scope
56
- # Rails 6 and 7 cannot mix `eager_load` and `includes` in the same scope
57
- if Rails::VERSION::MAJOR >= 7
58
- result = result.preload(preload_loads)
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
- result
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
- where = parse_aggregation(@filters)
17
- return @resource unless where
16
+ where_clause = parse_aggregation(@filters)
17
+ return @resource unless where_clause
18
18
 
19
19
  @joins.each do |join|
20
- current_resource = @resource.reflect_on_association(join.name).klass
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(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
- association = get_association_name_for_condition(field)
174
- quoted_table_name = ActiveRecord::Base.connection.quote_column_name(association)
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
- @user = forest_user
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
- has_body_attributes = attributes != nil
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
- # NOTICE: build IDs list.
57
- resources_getter.query_for_batch.find_in_batches() do |records|
58
- ids += records.map { |record| record.id }
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
- return ids
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
- # NOTICE: For performance reasons, do not optimize loading the data if there is no search or
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
- .select { |field| field.include? '.' }
95
- .map { |field| field.split('.').first.to_sym }
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
- .select { |association| SchemaUtils.model_included?(association.klass) }
99
- .map(&:name)
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
- if @params[:fields] && @params[:fields][@collection_name]
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
- # NOTICE: Populate the necessary associations for filters
140
- if @params[:filter]
141
- @params[:filter].each do |field, values|
142
- if field.include? ':'
143
- associations_for_query << field.split(':').first.to_sym
144
- @count_needs_includes = true
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
- if @params[:sort] && @params[:sort].include?('.')
152
- associations_for_query << @params[:sort].split('.').first.to_sym
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
- @fields_to_serialize | associations_for_query
115
+ apply_live_query_segment if @params[:segmentQuery]
116
+ @records = search_query
156
117
  end
157
118
 
158
- def search_query
159
- @search_query_builder.perform(@records)
119
+ def optimized_count
120
+ optimize_record_loading(@resource, @records).count
160
121
  end
161
122
 
162
- def prepare_query
163
- @records = get_resource
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
- if @segment && @segment.scope
166
- @records = @records.send(@segment.scope)
167
- elsif @segment && @segment.where
168
- @records = @records.where(@segment.where.call())
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
- # NOTICE: Live Query mode
172
- if @params[:segmentQuery]
173
- LiveQueryChecker.new(@params[:segmentQuery], 'Live Query Segment').validate()
174
-
175
- begin
176
- segmentQuery = @params[:segmentQuery].gsub(/\;\s*$/, '')
177
- @records = @records.where(
178
- "#{@resource.table_name}.#{@resource.primary_key} IN (SELECT id FROM (#{segmentQuery}) as ids)"
179
- )
180
- rescue => error
181
- error_message = "Live Query Segment: #{error.message}"
182
- FOREST_REPORTER.report error
183
- FOREST_LOGGER.error(error_message)
184
- raise ForestLiana::Errors::LiveQueryError.new(error_message)
185
- end
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
- @records = search_query
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 association?(field)
192
- @resource.reflect_on_association(field.to_sym).present?
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[:page][:number]
199
- if number && number.to_i > 0
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
- return 10 unless pagination?
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] && @params[:page][:number]
199
+ @params[:page]&.dig(:number)
218
200
  end
219
-
220
201
  end
221
202
  end
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.3.13"
2
+ VERSION = "9.3.15"
3
3
  end
@@ -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 "\"trees\".\"age\""}
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.13
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-07-19 00:00:00.000000000 Z
11
+ date: 2024-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails