Skip to content

Commit 2d43bc3

Browse files
feat: add polymorphic associations support (#640)
1 parent e7832d4 commit 2d43bc3

File tree

22 files changed

+389
-30
lines changed

22 files changed

+389
-30
lines changed

app/deserializers/forest_liana/resource_deserializer.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ def extract_relationships
6363
# ActionController::Parameters do not inherit from Hash anymore
6464
# since Rails 5.
6565
if (data.is_a?(Hash) || data.is_a?(ActionController::Parameters)) && data[:id]
66-
@attributes[name] = association.klass.find(data[:id])
66+
if (SchemaUtils.polymorphic?(association))
67+
@attributes[association.foreign_key] = data[:id]
68+
@attributes[association.foreign_type] = data[:type]
69+
else
70+
@attributes[name] = association.klass.find(data[:id])
71+
end
6772
elsif data.blank?
6873
@attributes[name] = nil
6974
end

app/helpers/forest_liana/query_helper.rb

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
module ForestLiana
22
module QueryHelper
33
def self.get_one_associations(resource)
4-
SchemaUtils.one_associations(resource)
5-
.select { |association| SchemaUtils.model_included?(association.klass) }
4+
associations = SchemaUtils.one_associations(resource)
5+
.select do |association|
6+
if SchemaUtils.polymorphic?(association)
7+
SchemaUtils.polymorphic_models(association).all? { |model| SchemaUtils.model_included?(model) }
8+
else
9+
SchemaUtils.model_included?(association.klass)
10+
end
11+
end
12+
13+
associations
614
end
715

816
def self.get_one_association_names_symbol(resource)
@@ -18,10 +26,19 @@ def self.get_tables_associated_to_relations_name(resource)
1826
associations_has_one = self.get_one_associations(resource)
1927

2028
associations_has_one.each do |association|
21-
if tables_associated_to_relations_name[association.table_name].nil?
22-
tables_associated_to_relations_name[association.table_name] = []
29+
if SchemaUtils.polymorphic?(association)
30+
SchemaUtils.polymorphic_models(association).each do |model|
31+
if tables_associated_to_relations_name[model.table_name].nil?
32+
tables_associated_to_relations_name[model.table_name] = []
33+
end
34+
tables_associated_to_relations_name[model.table_name] << association.name
35+
end
36+
else
37+
if tables_associated_to_relations_name[association.try(:table_name)].nil?
38+
tables_associated_to_relations_name[association.table_name] = []
39+
end
40+
tables_associated_to_relations_name[association.table_name] << association.name
2341
end
24-
tables_associated_to_relations_name[association.table_name] << association.name
2542
end
2643

2744
tables_associated_to_relations_name

app/serializers/forest_liana/serializer_factory.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,19 @@ def mixpanel_integration?
265265

266266
SchemaUtils.associations(active_record_class).each do |a|
267267
begin
268-
if SchemaUtils.model_included?(a.klass)
268+
if SchemaUtils.polymorphic?(a)
269+
serializer.send(serializer_association(a), a.name) {
270+
if [:has_one, :belongs_to].include?(a.macro)
271+
begin
272+
object.send(a.name)
273+
rescue ActiveRecord::RecordNotFound
274+
nil
275+
end
276+
else
277+
[]
278+
end
279+
}
280+
elsif SchemaUtils.model_included?(a.klass)
269281
serializer.send(serializer_association(a), a.name) {
270282
if [:has_one, :belongs_to].include?(a.macro)
271283
begin
@@ -369,6 +381,7 @@ def serializer_association(association)
369381

370382
def attributes(active_record_class)
371383
return [] if @is_smart_collection
384+
372385
active_record_class.column_names.select do |column_name|
373386
!association?(active_record_class, column_name)
374387
end
@@ -410,6 +423,9 @@ def association?(active_record_class, column_name)
410423
def foreign_keys(active_record_class)
411424
begin
412425
SchemaUtils.belongs_to_associations(active_record_class).map(&:foreign_key)
426+
SchemaUtils.belongs_to_associations(active_record_class)
427+
.select { |association| !SchemaUtils.polymorphic?(association) }
428+
.map(&:foreign_key)
413429
rescue => err
414430
# Association foreign_key triggers an error. Put the stacktrace and
415431
# returns no foreign keys.

app/services/forest_liana/apimap_sorter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class ApimapSorter
2929
'relationship',
3030
'widget',
3131
'validations',
32+
'polymorphic_referenced_models',
3233
]
3334
KEYS_ACTION = [
3435
'name',

app/services/forest_liana/base_getter.rb

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,23 @@ def compute_includes
3333
def optimize_record_loading(resource, records)
3434
instance_dependent_associations = instance_dependent_associations(resource)
3535

36+
polymorphic = []
3637
preload_loads = @includes.select do |name|
37-
targetModelConnection = resource.reflect_on_association(name).klass.connection
38-
targetModelDatabase = targetModelConnection.current_database if targetModelConnection.respond_to? :current_database
39-
resourceConnection = resource.connection
40-
resourceDatabase = resourceConnection.current_database if resourceConnection.respond_to? :current_database
38+
association = resource.reflect_on_association(name)
39+
if SchemaUtils.polymorphic?(association)
40+
polymorphic << association.name
41+
false
42+
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
4147

42-
targetModelDatabase != resourceDatabase
48+
targetModelDatabase != resourceDatabase
49+
end
4350
end + instance_dependent_associations
4451

45-
result = records.eager_load(@includes - preload_loads)
52+
result = records.eager_load(@includes - preload_loads - polymorphic)
4653

4754
# Rails 7 can mix `eager_load` and `preload` in the same scope
4855
# Rails 6 cannot mix `eager_load` and `preload` in the same scope

app/services/forest_liana/belongs_to_updater.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@ def initialize(resource, association, params)
1313
def perform
1414
begin
1515
@record = @resource.find(@params[:id])
16-
new_value = @association.klass.find(@data[:id]) if @data && @data[:id]
16+
if (SchemaUtils.polymorphic?(@association))
17+
if @data.nil?
18+
new_value = nil
19+
else
20+
association_klass = SchemaUtils.polymorphic_models(@association).select { |a| a.name.downcase == @data[:type] }.first
21+
new_value = association_klass.find(@data[:id]) if @data && @data[:id]
22+
end
23+
else
24+
new_value = @association.klass.find(@data[:id]) if @data && @data[:id]
25+
end
1726
@record.send("#{@association.name}=", new_value)
1827

1928
@record.save

app/services/forest_liana/has_many_getter.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,23 @@ def compute_includes
4040
@includes = @association.klass
4141
.reflect_on_all_associations
4242
.select do |association|
43-
inclusion = !association.options[:polymorphic] &&
44-
SchemaUtils.model_included?(association.klass) &&
45-
[:belongs_to, :has_and_belongs_to_many].include?(association.macro)
4643

47-
if @field_names_requested
48-
inclusion && @field_names_requested.include?(association.name)
44+
if SchemaUtils.polymorphic?(association)
45+
inclusion = SchemaUtils.polymorphic_models(association)
46+
.all? { |model| SchemaUtils.model_included?(model) } &&
47+
[:belongs_to, :has_and_belongs_to_many].include?(association.macro)
4948
else
50-
inclusion
49+
inclusion = SchemaUtils.model_included?(association.klass) &&
50+
[:belongs_to, :has_and_belongs_to_many].include?(association.macro)
5151
end
52-
end
53-
.map { |association| association.name.to_s }
52+
53+
if @field_names_requested
54+
inclusion && @field_names_requested.include?(association.name)
55+
else
56+
inclusion
57+
end
58+
end
59+
.map { |association| association.name }
5460
end
5561

5662
def field_names_requested

app/services/forest_liana/schema_adapter.rb

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,33 @@ def add_columns
241241
def add_associations
242242
SchemaUtils.associations(@model).each do |association|
243243
begin
244+
if SchemaUtils.polymorphic?(association) &&
245+
(ENV['ENABLE_SUPPORT_POLYMORPHISM'].present? && ENV['ENABLE_SUPPORT_POLYMORPHISM'].downcase == 'true')
246+
247+
collection.fields << {
248+
field: association.name.to_s,
249+
type: get_type_for_association(association),
250+
relationship: get_relationship_type(association),
251+
reference: "#{association.name.to_s}.id",
252+
inverse_of: @model.name.demodulize.underscore,
253+
is_filterable: false,
254+
is_sortable: true,
255+
is_read_only: false,
256+
is_required: false,
257+
is_virtual: false,
258+
default_value: nil,
259+
integration: nil,
260+
relationships: nil,
261+
widget: nil,
262+
validations: [],
263+
polymorphic_referenced_models: get_polymorphic_types(association)
264+
}
265+
266+
collection.fields = collection.fields.reject do |field|
267+
field[:field] == association.foreign_key || field[:field] == association.foreign_type
268+
end
244269
# NOTICE: Delete the association if the targeted model is excluded.
245-
if !SchemaUtils.model_included?(association.klass)
270+
elsif !SchemaUtils.model_included?(association.klass)
246271
field = collection.fields.find do |x|
247272
x[:field] == association.foreign_key
248273
end
@@ -275,6 +300,17 @@ def inverse_of(association)
275300
automatic_inverse_of(association)
276301
end
277302

303+
def get_polymorphic_types(relation)
304+
types = []
305+
ForestLiana.models.each do |model|
306+
unless model.reflect_on_all_associations.select { |association| association.options[:as] == relation.name.to_sym }.empty?
307+
types << model.name
308+
end
309+
end
310+
311+
types
312+
end
313+
278314
def automatic_inverse_of(association)
279315
name = association.active_record.name.demodulize.underscore
280316

app/services/forest_liana/schema_utils.rb

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ class SchemaUtils
44
def self.associations(active_record_class)
55
active_record_class.reflect_on_all_associations.select do |association|
66
begin
7-
!polymorphic?(association) && !is_active_type?(association.klass)
7+
if (ENV['ENABLE_SUPPORT_POLYMORPHISM'].present? && ENV['ENABLE_SUPPORT_POLYMORPHISM'].downcase == 'true')
8+
polymorphic?(association) ? true : !is_active_type?(association.klass)
9+
else
10+
!polymorphic?(association) && !is_active_type?(association.klass)
11+
end
12+
813
rescue
914
FOREST_LOGGER.warn "Unknown association #{association.name} on class #{active_record_class.name}"
1015
false
@@ -53,12 +58,30 @@ def self.tables_names
5358
ActiveRecord::Base.connection.tables
5459
end
5560

56-
private
57-
5861
def self.polymorphic?(association)
5962
association.options[:polymorphic]
6063
end
6164

65+
def self.klass(association)
66+
return association.klass unless polymorphic?(association)
67+
68+
69+
end
70+
71+
def self.polymorphic_models(relation)
72+
models = []
73+
ForestLiana.models.each do |model|
74+
unless model.reflect_on_all_associations.select { |association| association.options[:as] == relation.name.to_sym }.empty?
75+
models << model
76+
end
77+
end
78+
79+
models
80+
end
81+
82+
83+
private
84+
6285
def self.find_model_from_abstract_class(abstract_class, collection_name)
6386
abstract_class.subclasses.find do |subclass|
6487
if subclass.abstract_class?

lib/forest_liana/bootstrapper.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ module ForestLiana
55
class Bootstrapper
66
SCHEMA_FILENAME = File.join(Dir.pwd, '.forestadmin-schema.json')
77

8-
def initialize
8+
def initialize(reset_api_map = false)
9+
if reset_api_map
10+
ForestLiana.apimap = []
11+
ForestLiana.models = []
12+
end
13+
914
@integration_stripe_valid = false
1015
@integration_intercom_valid = false
1116

0 commit comments

Comments
 (0)