Skip to content

Commit

Permalink
feat: add polymorphic associations support (#640)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasalexandre9 committed Jan 18, 2024
1 parent e7832d4 commit 2d43bc3
Show file tree
Hide file tree
Showing 22 changed files with 389 additions and 30 deletions.
7 changes: 6 additions & 1 deletion app/deserializers/forest_liana/resource_deserializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ def extract_relationships
# ActionController::Parameters do not inherit from Hash anymore
# since Rails 5.
if (data.is_a?(Hash) || data.is_a?(ActionController::Parameters)) && data[:id]
@attributes[name] = association.klass.find(data[:id])
if (SchemaUtils.polymorphic?(association))
@attributes[association.foreign_key] = data[:id]
@attributes[association.foreign_type] = data[:type]
else
@attributes[name] = association.klass.find(data[:id])
end
elsif data.blank?
@attributes[name] = nil
end
Expand Down
27 changes: 22 additions & 5 deletions app/helpers/forest_liana/query_helper.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
module ForestLiana
module QueryHelper
def self.get_one_associations(resource)
SchemaUtils.one_associations(resource)
.select { |association| SchemaUtils.model_included?(association.klass) }
associations = SchemaUtils.one_associations(resource)
.select do |association|
if SchemaUtils.polymorphic?(association)
SchemaUtils.polymorphic_models(association).all? { |model| SchemaUtils.model_included?(model) }
else
SchemaUtils.model_included?(association.klass)
end
end

associations
end

def self.get_one_association_names_symbol(resource)
Expand All @@ -18,10 +26,19 @@ def self.get_tables_associated_to_relations_name(resource)
associations_has_one = self.get_one_associations(resource)

associations_has_one.each do |association|
if tables_associated_to_relations_name[association.table_name].nil?
tables_associated_to_relations_name[association.table_name] = []
if SchemaUtils.polymorphic?(association)
SchemaUtils.polymorphic_models(association).each do |model|
if tables_associated_to_relations_name[model.table_name].nil?
tables_associated_to_relations_name[model.table_name] = []
end
tables_associated_to_relations_name[model.table_name] << association.name
end
else
if tables_associated_to_relations_name[association.try(:table_name)].nil?
tables_associated_to_relations_name[association.table_name] = []
end
tables_associated_to_relations_name[association.table_name] << association.name
end
tables_associated_to_relations_name[association.table_name] << association.name
end

tables_associated_to_relations_name
Expand Down
18 changes: 17 additions & 1 deletion app/serializers/forest_liana/serializer_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,19 @@ def mixpanel_integration?

SchemaUtils.associations(active_record_class).each do |a|
begin
if SchemaUtils.model_included?(a.klass)
if SchemaUtils.polymorphic?(a)
serializer.send(serializer_association(a), a.name) {
if [:has_one, :belongs_to].include?(a.macro)
begin
object.send(a.name)
rescue ActiveRecord::RecordNotFound
nil
end
else
[]
end
}
elsif SchemaUtils.model_included?(a.klass)
serializer.send(serializer_association(a), a.name) {
if [:has_one, :belongs_to].include?(a.macro)
begin
Expand Down Expand Up @@ -369,6 +381,7 @@ def serializer_association(association)

def attributes(active_record_class)
return [] if @is_smart_collection

active_record_class.column_names.select do |column_name|
!association?(active_record_class, column_name)
end
Expand Down Expand Up @@ -410,6 +423,9 @@ def association?(active_record_class, column_name)
def foreign_keys(active_record_class)
begin
SchemaUtils.belongs_to_associations(active_record_class).map(&:foreign_key)
SchemaUtils.belongs_to_associations(active_record_class)
.select { |association| !SchemaUtils.polymorphic?(association) }
.map(&:foreign_key)
rescue => err
# Association foreign_key triggers an error. Put the stacktrace and
# returns no foreign keys.
Expand Down
1 change: 1 addition & 0 deletions app/services/forest_liana/apimap_sorter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class ApimapSorter
'relationship',
'widget',
'validations',
'polymorphic_referenced_models',
]
KEYS_ACTION = [
'name',
Expand Down
19 changes: 13 additions & 6 deletions app/services/forest_liana/base_getter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,23 @@ def compute_includes
def optimize_record_loading(resource, records)
instance_dependent_associations = instance_dependent_associations(resource)

polymorphic = []
preload_loads = @includes.select do |name|
targetModelConnection = resource.reflect_on_association(name).klass.connection
targetModelDatabase = targetModelConnection.current_database if targetModelConnection.respond_to? :current_database
resourceConnection = resource.connection
resourceDatabase = resourceConnection.current_database if resourceConnection.respond_to? :current_database
association = resource.reflect_on_association(name)
if SchemaUtils.polymorphic?(association)
polymorphic << association.name
false
else
targetModelConnection = association.klass.connection
targetModelDatabase = targetModelConnection.current_database if targetModelConnection.respond_to? :current_database
resourceConnection = resource.connection
resourceDatabase = resourceConnection.current_database if resourceConnection.respond_to? :current_database

targetModelDatabase != resourceDatabase
targetModelDatabase != resourceDatabase
end
end + instance_dependent_associations

result = records.eager_load(@includes - preload_loads)
result = records.eager_load(@includes - preload_loads - polymorphic)

# Rails 7 can mix `eager_load` and `preload` in the same scope
# Rails 6 cannot mix `eager_load` and `preload` in the same scope
Expand Down
11 changes: 10 additions & 1 deletion app/services/forest_liana/belongs_to_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ def initialize(resource, association, params)
def perform
begin
@record = @resource.find(@params[:id])
new_value = @association.klass.find(@data[:id]) if @data && @data[:id]
if (SchemaUtils.polymorphic?(@association))
if @data.nil?
new_value = nil
else
association_klass = SchemaUtils.polymorphic_models(@association).select { |a| a.name.downcase == @data[:type] }.first
new_value = association_klass.find(@data[:id]) if @data && @data[:id]
end
else
new_value = @association.klass.find(@data[:id]) if @data && @data[:id]
end
@record.send("#{@association.name}=", new_value)

@record.save
Expand Down
22 changes: 14 additions & 8 deletions app/services/forest_liana/has_many_getter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,23 @@ def compute_includes
@includes = @association.klass
.reflect_on_all_associations
.select do |association|
inclusion = !association.options[:polymorphic] &&
SchemaUtils.model_included?(association.klass) &&
[:belongs_to, :has_and_belongs_to_many].include?(association.macro)

if @field_names_requested
inclusion && @field_names_requested.include?(association.name)
if SchemaUtils.polymorphic?(association)
inclusion = SchemaUtils.polymorphic_models(association)
.all? { |model| SchemaUtils.model_included?(model) } &&
[:belongs_to, :has_and_belongs_to_many].include?(association.macro)
else
inclusion
inclusion = SchemaUtils.model_included?(association.klass) &&
[:belongs_to, :has_and_belongs_to_many].include?(association.macro)
end
end
.map { |association| association.name.to_s }

if @field_names_requested
inclusion && @field_names_requested.include?(association.name)
else
inclusion
end
end
.map { |association| association.name }
end

def field_names_requested
Expand Down
38 changes: 37 additions & 1 deletion app/services/forest_liana/schema_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,33 @@ def add_columns
def add_associations
SchemaUtils.associations(@model).each do |association|
begin
if SchemaUtils.polymorphic?(association) &&
(ENV['ENABLE_SUPPORT_POLYMORPHISM'].present? && ENV['ENABLE_SUPPORT_POLYMORPHISM'].downcase == 'true')

collection.fields << {
field: association.name.to_s,
type: get_type_for_association(association),
relationship: get_relationship_type(association),
reference: "#{association.name.to_s}.id",
inverse_of: @model.name.demodulize.underscore,
is_filterable: false,
is_sortable: true,
is_read_only: false,
is_required: false,
is_virtual: false,
default_value: nil,
integration: nil,
relationships: nil,
widget: nil,
validations: [],
polymorphic_referenced_models: get_polymorphic_types(association)
}

collection.fields = collection.fields.reject do |field|
field[:field] == association.foreign_key || field[:field] == association.foreign_type
end
# NOTICE: Delete the association if the targeted model is excluded.
if !SchemaUtils.model_included?(association.klass)
elsif !SchemaUtils.model_included?(association.klass)
field = collection.fields.find do |x|
x[:field] == association.foreign_key
end
Expand Down Expand Up @@ -275,6 +300,17 @@ def inverse_of(association)
automatic_inverse_of(association)
end

def get_polymorphic_types(relation)
types = []
ForestLiana.models.each do |model|
unless model.reflect_on_all_associations.select { |association| association.options[:as] == relation.name.to_sym }.empty?
types << model.name
end
end

types
end

def automatic_inverse_of(association)
name = association.active_record.name.demodulize.underscore

Expand Down
29 changes: 26 additions & 3 deletions app/services/forest_liana/schema_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ class SchemaUtils
def self.associations(active_record_class)
active_record_class.reflect_on_all_associations.select do |association|
begin
!polymorphic?(association) && !is_active_type?(association.klass)
if (ENV['ENABLE_SUPPORT_POLYMORPHISM'].present? && ENV['ENABLE_SUPPORT_POLYMORPHISM'].downcase == 'true')
polymorphic?(association) ? true : !is_active_type?(association.klass)
else
!polymorphic?(association) && !is_active_type?(association.klass)
end

rescue
FOREST_LOGGER.warn "Unknown association #{association.name} on class #{active_record_class.name}"
false
Expand Down Expand Up @@ -53,12 +58,30 @@ def self.tables_names
ActiveRecord::Base.connection.tables
end

private

def self.polymorphic?(association)
association.options[:polymorphic]
end

def self.klass(association)
return association.klass unless polymorphic?(association)


end

def self.polymorphic_models(relation)
models = []
ForestLiana.models.each do |model|
unless model.reflect_on_all_associations.select { |association| association.options[:as] == relation.name.to_sym }.empty?
models << model
end
end

models
end


private

def self.find_model_from_abstract_class(abstract_class, collection_name)
abstract_class.subclasses.find do |subclass|
if subclass.abstract_class?
Expand Down
7 changes: 6 additions & 1 deletion lib/forest_liana/bootstrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ module ForestLiana
class Bootstrapper
SCHEMA_FILENAME = File.join(Dir.pwd, '.forestadmin-schema.json')

def initialize
def initialize(reset_api_map = false)
if reset_api_map
ForestLiana.apimap = []
ForestLiana.models = []
end

@integration_stripe_valid = false
@integration_intercom_valid = false

Expand Down
1 change: 1 addition & 0 deletions lib/forest_liana/schema_file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SchemaFileUpdater
'relationship',
'widget',
'validations',
'polymorphic_referenced_models',
]
KEYS_VALIDATION = [
'message',
Expand Down
2 changes: 1 addition & 1 deletion lib/tasks/send_apimap.rake
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace :forest do
task(:send_apimap).clear
task send_apimap: :environment do
if ForestLiana.env_secret
bootstrapper = ForestLiana::Bootstrapper.new
bootstrapper = ForestLiana::Bootstrapper.new(true)
bootstrapper.synchronize(true)
else
puts 'Cannot send the Apimap, Forest cannot find your env_secret'
Expand Down
5 changes: 5 additions & 0 deletions spec/dummy/app/models/address.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Address < ActiveRecord::Base
self.table_name = 'addresses'

belongs_to :addressable, polymorphic: true
end
1 change: 1 addition & 0 deletions spec/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class User < ActiveRecord::Base
has_many :trees_owned, class_name: 'Tree', inverse_of: :owner
has_many :trees_cut, class_name: 'Tree', inverse_of: :cutter
has_many :addresses, as: :addressable

enum title: [ :king, :villager, :outlaw ]
end
12 changes: 12 additions & 0 deletions spec/dummy/db/migrate/20231117084236_create_addresses.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateAddresses < ActiveRecord::Migration[6.0]
def change
create_table :addresses do |t|
t.string :line1
t.string :city
t.string :zipcode
t.references :addressable, polymorphic: true, null: false

t.timestamps
end
end
end
13 changes: 12 additions & 1 deletion spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2022_07_27_114930) do
ActiveRecord::Schema.define(version: 2023_11_17_084236) do

create_table "addresses", force: :cascade do |t|
t.string "line1"
t.string "city"
t.string "zipcode"
t.string "addressable_type", null: false
t.integer "addressable_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["addressable_type", "addressable_id"], name: "index_addresses_on_addressable_type_and_addressable_id"
end

create_table "isle", force: :cascade do |t|
t.string "name"
Expand Down

0 comments on commit 2d43bc3

Please sign in to comment.