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 +4 -4
- data/app/controllers/forest_liana/router.rb +2 -0
- data/app/serializers/forest_liana/serializer_factory.rb +27 -4
- data/app/services/forest_liana/aggregation_helper.rb +42 -0
- data/app/services/forest_liana/base_getter.rb +6 -3
- data/app/services/forest_liana/has_many_getter.rb +4 -3
- data/app/services/forest_liana/leaderboard_stat_getter.rb +7 -15
- data/app/services/forest_liana/pie_stat_getter.rb +12 -27
- data/app/services/forest_liana/resources_getter.rb +178 -19
- data/app/services/forest_liana/schema_adapter.rb +16 -2
- data/app/services/forest_liana/schema_utils.rb +22 -0
- data/app/services/forest_liana/search_query_builder.rb +1 -1
- data/lib/forest_liana/active_record_override.rb +65 -0
- data/lib/forest_liana/engine.rb +7 -0
- data/lib/forest_liana/schema_file_updater.rb +1 -1
- data/lib/forest_liana/version.rb +1 -1
- data/spec/dummy/app/models/tree.rb +3 -1
- data/spec/helpers/forest_liana/query_helper_spec.rb +9 -4
- data/spec/requests/resources_spec.rb +30 -4
- data/spec/services/forest_liana/pie_stat_getter_spec.rb +182 -0
- data/spec/services/forest_liana/resources_getter_spec.rb +118 -23
- data/spec/services/forest_liana/schema_adapter_spec.rb +12 -1
- data/spec/services/forest_liana/serializer_factory_spec.rb +53 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c700fa60cbdb6da1e131e2e041d7bec408052bf6ce50304e51f2e6f6525e8d8
|
4
|
+
data.tar.gz: fdc53e76cf29afabdd6b18ec6a3279bd0e9bec3b467e8f8b16cb6cbb7dda87aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
64
|
-
|
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(
|
26
|
+
.order(Arel.sql("#{alias_name} DESC"))
|
22
27
|
.limit(@limit)
|
23
|
-
.
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
61
|
-
.map(&:name)
|
152
|
+
association.name
|
153
|
+
end
|
62
154
|
|
63
|
-
includes_for_smart_search =
|
64
|
-
|
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 =
|
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
|
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
|
-
|
98
|
-
|
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
|
-
|
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
|
@@ -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
|
data/lib/forest_liana/engine.rb
CHANGED
@@ -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
|