forest_liana 9.3.13 → 9.3.15

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: 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