forest_liana 9.11.3 → 9.15.1

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: dcd40a6f023ba4a2a7eddd15891ac11743dea8fee0df3378c42e604afbfe8c7e
4
- data.tar.gz: 61ceac08bd477c79ea3d6284114b6a9a08f2514758378f4f7a96c3845efd4f8f
3
+ metadata.gz: 1c700fa60cbdb6da1e131e2e041d7bec408052bf6ce50304e51f2e6f6525e8d8
4
+ data.tar.gz: fdc53e76cf29afabdd6b18ec6a3279bd0e9bec3b467e8f8b16cb6cbb7dda87aa
5
5
  SHA512:
6
- metadata.gz: e09c8a0a86cee9b238e35057b0e29e5b842fcf0a2a1380a57fb10bf00c9c2cf6cb5cf16bb9908d72fc4219966efa910742de673e5d0f9cd6696bbd83724d4f67
7
- data.tar.gz: 24ecaa8c8a9442ec3c677bb71f47dbf8a120cba326732f202264869d1a3188265a30caa14054ffbd13c218deb6b13294ef997f60d83f0f347faccbc60d32bc98
6
+ metadata.gz: fe65c60f75a320f96e826dc7b93e25106d0958c62f1157212496ba57eef33e9be08776ab104200b2eb9d7e772fc1f2cae58f811f2033d57e2969e877cf3dbc64
7
+ data.tar.gz: bf3c7eb151213fb126ef896d040369c2a3c65500e8e2ecbd292c06cbb016939fc607e295db9fc719b09e428c52ad7f4462bee211f7f532794be0d4895ef2d50d
@@ -37,6 +37,8 @@ class ForestLiana::Router
37
37
  end
38
38
  end
39
39
 
40
+ params["action"] = action
41
+ params["controller"] = "#{env["SCRIPT_NAME"]}/#{collection_name}".delete_prefix("/")
40
42
  controller.action(action.to_sym).call(env)
41
43
  rescue NoMethodError => exception
42
44
  FOREST_REPORTER.report exception
@@ -118,10 +118,6 @@ module ForestLiana
118
118
 
119
119
  if ret[:href].blank?
120
120
  begin
121
- if @options[:include].try(:include?, attribute_name.to_s)
122
- object.send(attribute_name)
123
- end
124
-
125
121
  SchemaUtils.many_associations(object.class).each do |a|
126
122
  if a.name == attribute_name
127
123
  ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/relationships/#{attribute_name}"
@@ -135,6 +131,33 @@ module ForestLiana
135
131
  ret
136
132
  end
137
133
 
134
+ def has_one_relationships
135
+ return {} if self.class.to_one_associations.nil?
136
+ data = {}
137
+ self.class.to_one_associations.each do |attribute_name, attr_data|
138
+ relation = object.class.reflect_on_all_associations.find { |a| a.name == attribute_name }
139
+
140
+ next if !should_include_attr?(attribute_name, attr_data)
141
+
142
+ unless relation.nil? || (relation.respond_to?(:polymorphic?) && relation.polymorphic?)
143
+ relation_class_name = ForestLiana.name_for(relation.klass).demodulize
144
+
145
+ if object.respond_to?(relation.foreign_key.to_sym) &&
146
+ @options[:fields][relation_class_name]&.size == 1 &&
147
+ @options[:fields][relation_class_name]&.include?(relation.klass.primary_key.to_sym)
148
+
149
+ attr_data[:attr_or_block] = proc {
150
+ foreign_key_value = object.send(relation.foreign_key.to_sym)
151
+ foreign_key_value.nil? ? nil : relation.klass.new(relation.klass.primary_key => foreign_key_value)
152
+ }
153
+ end
154
+ end
155
+
156
+ data[attribute_name] = attr_data
157
+ end
158
+ data
159
+ end
160
+
138
161
  private
139
162
 
140
163
  def intercom_integration?
@@ -0,0 +1,42 @@
1
+ module ForestLiana
2
+ module AggregationHelper
3
+ def resolve_field_path(field_param, default_field = 'id')
4
+ if field_param.blank?
5
+ default_field ||= @resource.primary_key || 'id'
6
+ return "#{@resource.table_name}.#{default_field}"
7
+ end
8
+
9
+ if field_param.include?(':')
10
+ association, field = field_param.split ':'
11
+ associated_resource = @resource.reflect_on_association(association.to_sym)
12
+ "#{associated_resource.table_name}.#{field}"
13
+ else
14
+ "#{@resource.table_name}.#{field_param}"
15
+ end
16
+ end
17
+
18
+ def aggregation_sql(type, field)
19
+ field_path = resolve_field_path(field)
20
+
21
+ case type
22
+ when 'sum'
23
+ "SUM(#{field_path})"
24
+ when 'count'
25
+ "COUNT(DISTINCT #{field_path})"
26
+ else
27
+ raise "Unsupported aggregator : #{type}"
28
+ end
29
+ end
30
+
31
+ def aggregation_alias(type, field)
32
+ case type
33
+ when 'sum'
34
+ "sum_#{field.downcase}"
35
+ when 'count'
36
+ 'count_id'
37
+ else
38
+ raise "Unsupported aggregator : #{type}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -19,13 +19,14 @@ module ForestLiana
19
19
 
20
20
  def compute_includes
21
21
  @includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@resource)
22
+ @optional_includes = []
22
23
  end
23
24
 
24
- def optimize_record_loading(resource, records)
25
+ def optimize_record_loading(resource, records, force_preload = true)
25
26
  polymorphic, preload_loads = analyze_associations(resource)
26
- result = records.eager_load(@includes.uniq - preload_loads - polymorphic)
27
+ result = records.eager_load(@includes.uniq - preload_loads - polymorphic - @optional_includes)
27
28
 
28
- result = result.preload(preload_loads) if Rails::VERSION::MAJOR >= 7
29
+ result = result.preload(preload_loads) if Rails::VERSION::MAJOR >= 7 && force_preload
29
30
 
30
31
  result
31
32
  end
@@ -46,6 +47,8 @@ module ForestLiana
46
47
  end
47
48
 
48
49
  def separate_database?(resource, association)
50
+ return false if SchemaUtils.polymorphic?(association)
51
+
49
52
  target_model_connection = association.klass.connection
50
53
  target_model_database = target_model_connection.current_database if target_model_connection.respond_to? :current_database
51
54
  resource_connection = resource.connection
@@ -37,6 +37,8 @@ module ForestLiana
37
37
  private
38
38
 
39
39
  def compute_includes
40
+ @optional_includes = []
41
+
40
42
  @includes = @association.klass
41
43
  .reflect_on_all_associations
42
44
  .select do |association|
@@ -60,9 +62,8 @@ module ForestLiana
60
62
  end
61
63
 
62
64
  def field_names_requested
63
- return nil unless @params[:fields] && @params[:fields][@collection_name]
64
- @params[:fields][@collection_name].split(',')
65
- .map { |name| name.to_sym }
65
+ fields = @params.dig(:fields, @collection_name)
66
+ Array(fields&.split(',')).map(&:to_sym)
66
67
  end
67
68
 
68
69
  def model_association
@@ -1,6 +1,9 @@
1
1
  module ForestLiana
2
2
  class LeaderboardStatGetter < StatGetter
3
+ include AggregationHelper
4
+
3
5
  def initialize(parent_model, params, forest_user)
6
+ @resource = parent_model
4
7
  @scoped_parent_model = get_scoped_model(parent_model, forest_user, params[:timezone])
5
8
  child_model = @scoped_parent_model.reflect_on_association(params[:relationshipFieldName]).klass
6
9
  @scoped_child_model = get_scoped_model(child_model, forest_user, params[:timezone])
@@ -14,13 +17,15 @@ module ForestLiana
14
17
  def perform
15
18
  includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@scoped_child_model)
16
19
 
20
+ alias_name = aggregation_alias(@aggregate, @aggregate_field)
21
+
17
22
  result = @scoped_child_model
18
23
  .joins(includes)
19
24
  .where({ @scoped_parent_model.name.downcase.to_sym => @scoped_parent_model })
20
25
  .group(@group_by)
21
- .order(order)
26
+ .order(Arel.sql("#{alias_name} DESC"))
22
27
  .limit(@limit)
23
- .send(@aggregate, @aggregate_field)
28
+ .pluck(@group_by, Arel.sql("#{aggregation_sql(@aggregate, @aggregate_field)} AS #{alias_name}"))
24
29
  .map { |key, value| { key: key, value: value } }
25
30
 
26
31
  @record = Model::Stat.new(value: result)
@@ -33,18 +38,5 @@ module ForestLiana
33
38
 
34
39
  FiltersParser.new(scope_filters, model, timezone, @params).apply_filters
35
40
  end
36
-
37
- def order
38
- order = 'DESC'
39
-
40
- # NOTICE: The generated alias for a count is "count_all", for a sum the
41
- # alias looks like "sum_#{aggregate_field}"
42
- if @aggregate == 'sum'
43
- field = @aggregate_field.downcase
44
- else
45
- field = 'all'
46
- end
47
- "#{@aggregate}_#{field} #{order}"
48
- end
49
41
  end
50
42
  end
@@ -1,5 +1,6 @@
1
1
  module ForestLiana
2
2
  class PieStatGetter < StatGetter
3
+ include AggregationHelper
3
4
  attr_accessor :record
4
5
 
5
6
  def perform
@@ -13,11 +14,16 @@ module ForestLiana
13
14
  resource = FiltersParser.new(filters, resource, @params[:timezone], @params).apply_filters
14
15
  end
15
16
 
16
- result = resource
17
- .group(groupByFieldName)
18
- .order(order)
19
- .send(@params[:aggregator].downcase, @params[:aggregateFieldName])
20
- .map do |key, value|
17
+ aggregation_type = @params[:aggregator].downcase
18
+ aggregation_field = @params[:aggregateFieldName]
19
+ alias_name = aggregation_alias(aggregation_type, aggregation_field)
20
+
21
+ resource = resource
22
+ .group(groupByFieldName)
23
+ .order(Arel.sql("#{alias_name} DESC"))
24
+ .pluck(groupByFieldName, Arel.sql("#{aggregation_sql(aggregation_type, aggregation_field)} AS #{alias_name}"))
25
+
26
+ result = resource.map do |key, value|
21
27
  # NOTICE: Display the enum name instead of an integer if it is an
22
28
  # "Enum" field type on old Rails version (before Rails
23
29
  # 5.1.3).
@@ -38,28 +44,7 @@ module ForestLiana
38
44
  end
39
45
 
40
46
  def groupByFieldName
41
- if @params[:groupByFieldName].include? ':'
42
- association, field = @params[:groupByFieldName].split ':'
43
- resource = @resource.reflect_on_association(association.to_sym)
44
- "#{resource.table_name}.#{field}"
45
- else
46
- "#{@resource.table_name}.#{@params[:groupByFieldName]}"
47
- end
48
- end
49
-
50
- def order
51
- order = 'DESC'
52
-
53
- # NOTICE: The generated alias for a count is "count_all", for a sum the
54
- # alias looks like "sum_#{aggregateFieldName}"
55
- if @params[:aggregator].downcase == 'sum'
56
- field = @params[:aggregateFieldName].downcase
57
- else
58
- # `count_id` is required only for rails v5
59
- field = Rails::VERSION::MAJOR == 5 || @includes.size > 0 ? 'id' : 'all'
60
- end
61
- "#{@params[:aggregator].downcase}_#{field} #{order}"
47
+ resolve_field_path(@params[:groupByFieldName])
62
48
  end
63
-
64
49
  end
65
50
  end
@@ -30,7 +30,20 @@ module ForestLiana
30
30
  end
31
31
 
32
32
  def perform
33
- @records = optimize_record_loading(@resource, @records)
33
+ polymorphic_association, preload_loads = analyze_associations(@resource)
34
+ includes = @includes.uniq - polymorphic_association - preload_loads - @optional_includes
35
+ has_smart_fields = Array(@params.dig(:fields, @collection_name)&.split(',')).any? do |field|
36
+ ForestLiana::SchemaHelper.is_smart_field?(@resource, field)
37
+ end
38
+
39
+ if includes.empty? || has_smart_fields
40
+ @records = optimize_record_loading(@resource, @records, false)
41
+ else
42
+ select = compute_select_fields
43
+ @records = optimize_record_loading(@resource, @records, false).references(includes).select(*select)
44
+ end
45
+
46
+ @records
34
47
  end
35
48
 
36
49
  def count
@@ -42,31 +55,122 @@ module ForestLiana
42
55
  end
43
56
 
44
57
  def records
45
- @records.offset(offset).limit(limit).to_a
58
+ records = @records.offset(offset).limit(limit).to_a
59
+ polymorphic_association, preload_loads = analyze_associations(@resource)
60
+
61
+ if polymorphic_association.any? && Rails::VERSION::MAJOR >= 7
62
+ preloader = ActiveRecord::Associations::Preloader.new(records: records, associations: polymorphic_association)
63
+ preloader.loaders
64
+ preloader.branches.each do |branch|
65
+ branch.loaders.each do |loader|
66
+ records_by_owner = loader.records_by_owner
67
+ records_by_owner.each do |record, association|
68
+ record_index = records.find_index { |r| r.id == record.id }
69
+ records[record_index].define_singleton_method(branch.association) do
70
+ association.first
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ preload_cross_database_associations(records, preload_loads)
78
+
79
+ records
80
+ end
81
+
82
+ def preload_cross_database_associations(records, preload_loads)
83
+ preload_loads.each do |association_name|
84
+ association = @resource.reflect_on_association(association_name)
85
+ next unless separate_database?(@resource, association)
86
+
87
+ columns = columns_for_cross_database_association(association_name)
88
+ if association.macro == :belongs_to
89
+ foreign_key = association.foreign_key
90
+ primary_key = association.klass.primary_key
91
+
92
+ ids = records.map { |r| r.public_send(foreign_key) }.compact.uniq
93
+ next if ids.empty?
94
+
95
+ associated = association.klass.where(primary_key => ids)
96
+ .select(columns)
97
+ .index_by { |record| record.public_send(primary_key) }
98
+
99
+ records.each do |record|
100
+ record.define_singleton_method(association_name) do
101
+ associated[record.send(foreign_key.to_sym)] || nil
102
+ end
103
+ end
104
+ end
105
+
106
+ if association.macro == :has_one
107
+ foreign_key = association.foreign_key
108
+ primary_key = association.active_record_primary_key
109
+
110
+ ids = records.map { |r| r.public_send(primary_key) }.compact.uniq
111
+ next if ids.empty?
112
+
113
+ associated = association.klass.where(foreign_key => ids)
114
+ .select(columns)
115
+ .index_by { |record| record.public_send(foreign_key.to_sym) }
116
+
117
+ records.each do |record|
118
+ record.define_singleton_method(association_name) do
119
+ associated[record.send(primary_key.to_sym)] || nil
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def columns_for_cross_database_association(association_name)
127
+ association = @resource.reflect_on_association(association_name)
128
+
129
+ # Always include all columns of the associated model to avoid missing attribute errors
130
+ columns = association.klass.column_names.map(&:to_sym)
131
+
132
+ # Ensure the foreign key is present for manual binding (especially for has_one)
133
+ columns << association.foreign_key.to_sym if association.macro == :has_one
134
+
135
+ columns.uniq
46
136
  end
47
137
 
48
138
  def compute_includes
49
139
  associations_has_one = ForestLiana::QueryHelper.get_one_associations(@resource)
50
140
 
51
- includes = associations_has_one.map(&:name)
52
- includes_for_smart_search = []
141
+ @optional_includes = []
142
+ if @field_names_requested && @params['searchExtended'].to_i != 1
143
+ includes = associations_has_one.map do |association|
144
+ association_name = association.name.to_s
53
145
 
54
- if @collection && @collection.search_fields
55
- includes_for_smart_search = @collection.search_fields
56
- .select { |field| field.include? '.' }
57
- .map { |field| field.split('.').first.to_sym }
146
+ fields = @params[:fields]&.[](association_name)&.split(',')
147
+ if fields&.size == 1 && fields.include?(association.klass.primary_key)
148
+ @field_names_requested << association.foreign_key
149
+ @optional_includes << association.name
150
+ end
58
151
 
59
- includes_has_many = SchemaUtils.many_associations(@resource)
60
- .select { |association| SchemaUtils.model_included?(association.klass) }
61
- .map(&:name)
152
+ association.name
153
+ end
62
154
 
63
- includes_for_smart_search = includes_for_smart_search & includes_has_many
64
- end
155
+ includes_for_smart_search = []
156
+ if @collection && @collection.search_fields
157
+ includes_for_smart_search = @collection.search_fields
158
+ .select { |field| field.include? '.' }
159
+ .map { |field| field.split('.').first.to_sym }
160
+
161
+ includes_has_many = SchemaUtils.many_associations(@resource)
162
+ .select { |association| SchemaUtils.model_included?(association.klass) }
163
+ .map(&:name)
164
+
165
+ includes_for_smart_search = includes_for_smart_search & includes_has_many
166
+ end
65
167
 
66
- if @field_names_requested
67
168
  @includes = (includes & @field_names_requested).concat(includes_for_smart_search)
68
169
  else
69
- @includes = includes
170
+ @includes = associations_has_one
171
+ # Avoid eager loading has_one associations pointing to a different database as ORM can't join cross databases
172
+ .reject { |association| separate_database?(@resource, association) }
173
+ .map(&:name)
70
174
  end
71
175
  end
72
176
 
@@ -85,7 +189,7 @@ module ForestLiana
85
189
  end
86
190
 
87
191
  def field_names_requested
88
- return nil unless @params[:fields] && @params[:fields][@collection_name]
192
+ return [] unless @params.dig(:fields, @collection_name)
89
193
 
90
194
  associations_for_query = extract_associations_from_filter
91
195
  associations_for_query << @params[:sort].split('.').first.to_sym if @params[:sort]&.include?('.')
@@ -94,15 +198,29 @@ module ForestLiana
94
198
 
95
199
  def extract_associations_from_filter
96
200
  associations = []
97
- @params[:filter]&.each do |field, _|
98
- if field.include?(':')
201
+
202
+ filters = @params[:filters]
203
+ filters = JSON.parse(filters) if filters.is_a?(String)
204
+
205
+ conditions = []
206
+
207
+ if filters.is_a?(Hash) && filters.key?('conditions')
208
+ conditions = filters['conditions']
209
+ elsif filters.is_a?(Hash) && filters.key?('field')
210
+ conditions = [filters]
211
+ end
212
+
213
+ conditions.each do |condition|
214
+ field = condition['field']
215
+ if field&.include?(':')
99
216
  associations << field.split(':').first.to_sym
100
217
  @count_needs_includes = true
101
218
  end
102
219
  end
220
+
103
221
  @count_needs_includes = true if @params[:search]
104
222
 
105
- associations
223
+ associations.uniq
106
224
  end
107
225
 
108
226
  def prepare_query
@@ -210,5 +328,46 @@ module ForestLiana
210
328
  def pagination?
211
329
  @params[:page]&.dig(:number)
212
330
  end
331
+
332
+ def compute_select_fields
333
+ select = ['_forest_admin_eager_load']
334
+ @field_names_requested.each do |path|
335
+ association = get_one_association(path)
336
+ if association
337
+ while association.options[:through]
338
+ association = get_one_association(association.options[:through])
339
+ end
340
+
341
+ if SchemaUtils.polymorphic?(association)
342
+ select << "#{@resource.table_name}.#{association.foreign_type}"
343
+ end
344
+ select << "#{@resource.table_name}.#{association.foreign_key}"
345
+ end
346
+
347
+ fields = @params[:fields]&.[](path)&.split(',')
348
+ if fields
349
+ association = get_one_association(path)
350
+ table_name = association.table_name
351
+
352
+ fields.each do |association_path|
353
+ if ForestLiana::SchemaHelper.is_smart_field?(association.klass, association_path)
354
+ association.klass.attribute_names.each { |attribute| select << "#{table_name}.#{attribute}" }
355
+ else
356
+ select << "#{table_name}.#{association_path}"
357
+ end
358
+ end
359
+ else
360
+ select << "#{@resource.table_name}.#{path}"
361
+ end
362
+ end
363
+
364
+ select.uniq
365
+ end
366
+
367
+ def get_one_association(name)
368
+ ForestLiana::QueryHelper.get_one_associations(@resource)
369
+ .select { |association| association.name == name.to_sym }
370
+ .first
371
+ end
213
372
  end
214
373
  end
@@ -278,7 +278,13 @@ module ForestLiana
278
278
  field[:field] = association.name
279
279
  field[:inverse_of] = inverse_of(association)
280
280
  field[:relationship] = get_relationship_type(association)
281
- # NOTICE: Create the fields of hasOne, HasMany, … relationships.
281
+
282
+ ForestLiana::SchemaUtils.disable_filter_and_sort_if_cross_db!(
283
+ field,
284
+ association.name.to_s,
285
+ ForestLiana.name_for(@model)
286
+ )
287
+ # NOTICE: Create the fields of hasOne, HasMany, … relationships.
282
288
  else
283
289
  collection.fields << get_schema_for_association(association)
284
290
  end
@@ -346,7 +352,7 @@ module ForestLiana
346
352
  end
347
353
 
348
354
  def get_schema_for_association(association)
349
- {
355
+ opts ={
350
356
  field: association.name.to_s,
351
357
  type: get_type_for_association(association),
352
358
  relationship: get_relationship_type(association),
@@ -363,6 +369,14 @@ module ForestLiana
363
369
  widget: nil,
364
370
  validations: []
365
371
  }
372
+
373
+ ForestLiana::SchemaUtils.disable_filter_and_sort_if_cross_db!(
374
+ opts,
375
+ association.name.to_s,
376
+ ForestLiana.name_for(@model)
377
+ )
378
+
379
+ opts
366
380
  end
367
381
 
368
382
  def get_relationship_type(association)
@@ -126,5 +126,27 @@ module ForestLiana
126
126
  def self.is_active_type? model
127
127
  Object.const_defined?('ActiveType::Object') && model < ActiveType::Object
128
128
  end
129
+
130
+ def self.disable_filter_and_sort_if_cross_db!(opts, name, collection_name)
131
+ return unless opts[:reference]
132
+
133
+ assoc_name = opts[:reference].split('.').first&.underscore&.to_sym || name
134
+ model = find_model_from_collection_name(collection_name)
135
+ return unless model
136
+
137
+ association = model.reflect_on_association(assoc_name)
138
+ return unless association
139
+ return if polymorphic?(association)
140
+
141
+ model_db = model.connection_db_config.database
142
+ assoc_db = association.klass.connection_db_config.database
143
+
144
+ if model_db != assoc_db
145
+ opts[:is_filterable] = false
146
+ opts[:is_sortable] = false
147
+ end
148
+ rescue => e
149
+ FOREST_LOGGER.warn("Could not evaluate cross-db association for #{name}: #{e.message}")
150
+ end
129
151
  end
130
152
  end
@@ -150,7 +150,7 @@ module ForestLiana
150
150
  conditions.join(' OR '),
151
151
  search_value_for_string: "%#{@search.downcase}%",
152
152
  search_value_for_uuid: @search.to_s
153
- )
153
+ ) unless conditions.empty?
154
154
  end
155
155
 
156
156
  @records
@@ -0,0 +1,65 @@
1
+ module ForestLiana
2
+ module ActiveRecordOverride
3
+ module Associations
4
+ require 'active_record/associations/join_dependency'
5
+ module JoinDependency
6
+ def apply_column_aliases(relation)
7
+ if !(@join_root_alias = relation.select_values.empty?) &&
8
+ relation.select_values.first.to_s == '_forest_admin_eager_load'
9
+
10
+ relation.select_values.shift
11
+ used_cols = {}
12
+ # Find and expand out all column names being used in select(...)
13
+ new_select_values = relation.select_values.map(&:to_s).each_with_object([]) do |col, select|
14
+ unless col.include?(' ') # Pass it through if it's some expression (No chance for a simple column reference)
15
+ col = if (col_parts = col.split('.')).length == 1
16
+ [col]
17
+ else
18
+ [col_parts[0..-2].join('.'), col_parts.last]
19
+ end
20
+ used_cols[col] = nil
21
+ end
22
+ select << col
23
+ end
24
+
25
+ if new_select_values.present?
26
+ relation.select_values = new_select_values
27
+ else
28
+ relation.select_values.clear
29
+ end
30
+
31
+ @aliases ||= ActiveRecord::Associations::JoinDependency::Aliases.new(join_root.each_with_index.map do |join_part, i|
32
+ join_alias = join_part.table&.table_alias || join_part.table_name
33
+ keys = [join_part.base_klass.primary_key] # Always include the primary key
34
+
35
+ # # %%% Optional to include all foreign keys:
36
+ # keys.concat(join_part.base_klass.reflect_on_all_associations.select { |a| a.belongs_to? }.map(&:foreign_key))
37
+ # Add foreign keys out to referenced tables that we belongs_to
38
+ join_part.children.each { |child| keys << child.reflection.foreign_key if child.reflection.belongs_to? }
39
+
40
+ # Add the foreign key that got us here -- "the train we rode in on" -- if we arrived from
41
+ # a has_many or has_one:
42
+ if join_part.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation) &&
43
+ !join_part.reflection.belongs_to?
44
+ keys << join_part.reflection.foreign_key
45
+ end
46
+ keys = keys.compact # In case we're using composite_primary_keys
47
+ j = 0
48
+ columns = join_part.column_names.each_with_object([]) do |column_name, s|
49
+ # Include columns chosen in select(...) as well as the PK and any relevant FKs
50
+ if used_cols.keys.find { |c| (c.length == 1 || c.first == join_alias) && c.last == column_name } ||
51
+ keys.find { |c| c == column_name }
52
+ s << ActiveRecord::Associations::JoinDependency::Aliases::Column.new(column_name, "t#{i}_r#{j}")
53
+ end
54
+ j += 1
55
+ end
56
+ ActiveRecord::Associations::JoinDependency::Aliases::Table.new(join_part, columns)
57
+ end)
58
+ relation.select_values.clear
59
+ end
60
+ relation._select!(-> { aliases.columns })
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -7,6 +7,7 @@ require 'jwt'
7
7
  require 'bcrypt'
8
8
  require_relative 'bootstrapper'
9
9
  require_relative 'collection'
10
+ require_relative 'active_record_override'
10
11
 
11
12
  module Rack
12
13
  class Cors
@@ -90,6 +91,12 @@ module ForestLiana
90
91
  end
91
92
  end
92
93
 
94
+ initializer 'forest_liana.override_active_record_dependency' do
95
+ ActiveSupport.on_load(:active_record) do
96
+ ActiveRecord::Associations::JoinDependency.prepend(ForestLiana::ActiveRecordOverride::Associations::JoinDependency)
97
+ end
98
+ end
99
+
93
100
  config.after_initialize do |app|
94
101
  if error
95
102
  FOREST_REPORTER.report error