Skip to content

Commit

Permalink
feat: add polymorphic support (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasalexandre9 authored Aug 19, 2024
1 parent 8bd12dd commit 80566a5
Show file tree
Hide file tree
Showing 76 changed files with 2,094 additions and 220 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
bundle install
- name: Test
run: cd packages/${{ matrix.package }} && bundle install && bundle exec rspec --color --format doc && cd -
run: cd packages/${{ matrix.package }} && BUNDLE_GEMFILE=Gemfile-test bundle install && BUNDLE_GEMFILE=Gemfile-test bundle exec rspec --color --format doc && cd -

- name: Upload coverage
uses: actions/upload-artifact@v3
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ vendor

# GEM
Gemfile.lock
Gemfile-test.lock
*.gem
pkg/
2 changes: 1 addition & 1 deletion .releaserc.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ module.exports = {
'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/schema_emitter.rb',
'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/version.rb',
'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/version.rb',
'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_customizer/version.rb',
'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/version.rb',
'packages/forest_admin_rails/lib/forest_admin_rails/version.rb',
'package.json'
],
Expand Down
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ Metrics/MethodLength:
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/validations/field_validator.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field_factory.rb'
- 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb'
- 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb'

Metrics/BlockLength:
Exclude:
Expand All @@ -239,11 +240,13 @@ Metrics/ClassLength:
- 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/action/actions.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/frontend_validation_utils.rb'
- 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb'
- 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb'
- 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb'
- 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb'
- 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb'
- 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb'
- 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb'
Expand All @@ -253,6 +256,7 @@ Metrics/ClassLength:
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/comparisons.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/validations/rules.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_action_field_widget.rb'
- 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb'

Style/OpenStructUse:
Exclude:
Expand Down
5 changes: 2 additions & 3 deletions packages/forest_admin_agent/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ source "https://rubygems.org"

gemspec

gem 'forest_admin_datasource_customizer'
gem 'forest_admin_datasource_toolkit'

group :development, :test do
gem 'forest_admin_datasource_customizer'
gem 'forest_admin_datasource_toolkit'
gem 'rspec', '~> 3.0'
gem 'simplecov', '~> 0.22', require: false
gem 'simplecov-html', '~> 0.12.3'
Expand Down
12 changes: 12 additions & 0 deletions packages/forest_admin_agent/Gemfile-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
source "https://rubygems.org"

gemspec

group :development, :test do
gem 'forest_admin_datasource_customizer', path: '../forest_admin_datasource_customizer'
gem 'forest_admin_datasource_toolkit', path: '../forest_admin_datasource_toolkit'
gem 'rspec', '~> 3.0'
gem 'simplecov', '~> 0.22', require: false
gem 'simplecov-html', '~> 0.12.3'
gem 'simplecov_json_formatter', '~> 0.1.4'
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def format_attributes(args)
record[schema.foreign_key] = value['data'][schema.foreign_key_target] if schema.type == 'ManyToOne'
end

record
record || {}
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ def build(args = {})
super

relation = @collection.schema[:fields][args[:params]['relation_name']]
@child_collection = @datasource.get_collection(relation.foreign_collection)
@child_collection = if relation.type == 'PolymorphicManyToOne'
@datasource.get_collection(args[:params]['data']['type'])
else
@datasource.get_collection(relation.foreign_collection)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ def handle_request_bulk(args = {})
def delete_records(args, selection_ids)
condition_tree_ids = ConditionTree::ConditionTreeFactory.match_records(@collection, selection_ids[:ids])
condition_tree_ids = condition_tree_ids.inverse if selection_ids[:are_excluded]

@collection.schema[:fields].each_value do |field_schema|
next unless field_schema.type == 'PolymorphicOneToOne' || field_schema.type == 'PolymorphicOneToMany'

condition_tree = Nodes::ConditionTreeBranch.new(
'And',
[
Nodes::ConditionTreeLeaf.new(field_schema.origin_key, Operators::IN,
selection_ids[:ids].map { |value| value['id'] }),
Nodes::ConditionTreeLeaf.new(field_schema.origin_type_field, Operators::EQUAL,
@collection.name.gsub('__', '::'))
]
)
filter = Filter.new(condition_tree: condition_tree)
@datasource.get_collection(field_schema.foreign_collection)
.update(@caller, filter, { field_schema.origin_key => nil,
field_schema.origin_type_field => nil })
end

filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def handle_request(args = {})
class_name: @collection.name,
is_collection: true,
serializer: Serializer::ForestSerializer,
include: projection.relations.keys,
include: projection.relations(only_keys: true),
meta: handle_search_decorator(args[:params]['search'], records)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ def handle_request(args = {})
target_relation_id = Utils::Id.unpack_id(@child_collection, args[:params]['data'][0]['id'], with_key: true)
relation = Schema.get_to_many_relation(@collection, args[:params]['relation_name'])

if relation.type == 'OneToMany'
case relation.type
when 'OneToMany'
associate_one_to_many(relation, parent_id, target_relation_id)
else
when 'ManyToMany'
associate_many_to_many(relation, parent_id, target_relation_id)
when 'PolymorphicOneToMany'
associate_polymorphic_one_to_many(relation, parent_id, target_relation_id)
end

{ content: nil, status: 204 }
Expand All @@ -55,6 +58,27 @@ def associate_one_to_many(relation, parent_id, target_relation_id)
@child_collection.update(@caller, filter, { relation.origin_key => value })
end

def associate_polymorphic_one_to_many(relation, parent_id, target_relation_id)
id = Schema.primary_keys(@child_collection)[0]
value = Collection.get_value(@child_collection, @caller, target_relation_id, id)
filter = Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
ConditionTree::Nodes::ConditionTreeLeaf.new(id, 'Equal', value),
@permissions.get_scope(@collection)
]
)
)

value = Collection.get_value(@collection, @caller, parent_id, relation.origin_key_target)

@child_collection.update(
@caller,
filter,
{ relation.origin_key => value, relation.origin_type_field => @collection.name.gsub('__', '::') }
)
end

def associate_many_to_many(relation, parent_id, target_relation_id)
id = Schema.primary_keys(@child_collection)[0]
foreign_value = Collection.get_value(@child_collection, @caller, target_relation_id, id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def handle_request(args = {})
filter = get_base_foreign_filter(args)
relation = Schema.get_to_many_relation(@collection, args[:params]['relation_name'])

if relation.type == 'OneToMany'
if relation.type == 'OneToMany' || relation.type == 'PolymorphicOneToMany'
dissociate_or_delete_one_to_many(relation, args[:params]['relation_name'], parent_id, is_delete_mode,
filter)
else
Expand All @@ -47,7 +47,12 @@ def dissociate_or_delete_one_to_many(relation, relation_name, parent_id, is_dele
if is_delete_mode
@child_collection.delete(@caller, foreign_filter)
else
@child_collection.update(@caller, foreign_filter, { relation.origin_key => nil })
patch = if relation.type == 'PolymorphicOneToMany'
{ relation.origin_key => nil, relation.origin_type_field => nil }
else
{ relation.origin_key => nil }
end
@child_collection.update(@caller, foreign_filter, patch)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class UpdateRelated < AbstractRelatedRoute
include ForestAdminAgent::Builder
include ForestAdminDatasourceToolkit::Utils
include ForestAdminDatasourceToolkit::Components::Query

def setup_routes
add_route(
'forest_related_update',
Expand All @@ -26,14 +27,19 @@ def handle_request(args = {})
relation = @collection.schema[:fields][args[:params]['relation_name']]
parent_id = Utils::Id.unpack_id(@collection, args[:params]['id'])

linked_id = if (id = args.dig(:params, :data, :id))
linked_id = if (id = args.dig(:params, 'data', 'id'))
Utils::Id.unpack_id(@child_collection, id)
end

if relation.type == 'ManyToOne'
case relation.type
when 'ManyToOne'
update_many_to_one(relation, parent_id, linked_id)
elsif relation.type == 'OneToOne'
when 'PolymorphicManyToOne'
update_polymorphic_many_to_one(relation, parent_id, linked_id)
when 'OneToOne'
update_one_to_one(relation, parent_id, linked_id)
when 'PolymorphicOneToOne'
update_polymorphic_one_to_one(relation, parent_id, linked_id)
end

{ content: nil, status: 204 }
Expand All @@ -49,16 +55,104 @@ def update_many_to_one(relation, parent_id, linked_id)
@collection.update(@caller, Filter.new(condition_tree: fk_owner), { relation.foreign_key => foreign_value })
end

def update_polymorphic_many_to_one(relation, parent_id, linked_id)
foreign_value = if linked_id
Collection.get_value(
@child_collection,
@caller,
linked_id,
relation.foreign_key_targets[@child_collection.name]
)
end

polymorphic_type = @child_collection.name.gsub('__', '::')
fk_owner = ConditionTree::ConditionTreeFactory.match_ids(@collection, [parent_id])
@collection.update(
@caller,
Filter.new(condition_tree: fk_owner),
{
relation.foreign_key => foreign_value,
relation.foreign_key_type_field => polymorphic_type
}
)
end

def update_polymorphic_one_to_one(relation, parent_id, linked_id)
origin_value = Collection.get_value(@collection, @caller, parent_id, relation.origin_key_target)

break_old_polymorphic_one_to_one_relationship(relation, origin_value, linked_id)
create_new_polymorphic_one_to_one_relationship(relation, origin_value, linked_id)
end

def update_one_to_one(relation, parent_id, linked_id)
origin_value = Collection.get_value(@collection, @caller, parent_id, relation.origin_key_target)

break_old_one_to_one_relationship(nil, relation, origin_value, linked_id)
create_new_one_to_one_relationship(nil, relation, origin_value, linked_id)
break_old_one_to_one_relationship(relation, origin_value, linked_id)
create_new_one_to_one_relationship(relation, origin_value, linked_id)
end

def break_old_one_to_one_relationship(_scope, relation, origin_value, linked_id)
def break_old_polymorphic_one_to_one_relationship(relation, origin_value, linked_id)
linked_id ||= []

old_fk_owner_filter = Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
@permissions.get_scope(@collection),
ConditionTree::Nodes::ConditionTreeBranch.new(
'And',
[
ConditionTree::Nodes::ConditionTreeLeaf.new(
relation.origin_key,
ConditionTree::Operators::EQUAL,
origin_value
),
ConditionTree::Nodes::ConditionTreeLeaf.new(
relation.origin_type_field,
ConditionTree::Operators::EQUAL,
@collection.name.gsub('__', '::')
)
]
),
# Don't set the new record's field to null
# if it's already initialized with the right value
ConditionTree::ConditionTreeFactory.match_ids(@child_collection, [linked_id]).inverse
]
)
)

result = @child_collection.aggregate(@caller, old_fk_owner_filter, Aggregation.new(operation: 'Count'), 1)
return unless !(result[0]['value']).nil? && (result[0]['value']).positive?

# Avoids updating records to null if it's not authorized by the ORM
# and if there is no record to update (the filter returns no record)

@child_collection.update(
@caller,
old_fk_owner_filter,
{ relation.origin_key => nil, relation.origin_type_field => nil }
)
end

def create_new_polymorphic_one_to_one_relationship(relation, origin_value, linked_id)
return unless linked_id

new_fk_owner = ConditionTree::ConditionTreeFactory.match_ids(@child_collection, [linked_id])

@child_collection.update(
@caller,
Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
@permissions.get_scope(@collection), new_fk_owner
]
)
),
{ relation.origin_key => origin_value, relation.origin_type_field => @collection.name.gsub('__', '::') }
)
end

def break_old_one_to_one_relationship(relation, origin_value, linked_id)
linked_id ||= []
old_fk_owner_filter = Filter.new(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
Expand All @@ -67,26 +161,24 @@ def break_old_one_to_one_relationship(_scope, relation, origin_value, linked_id)
relation.origin_key,
ConditionTree::Operators::EQUAL,
origin_value
)
].push(
),
# Don't set the new record's field to null
# if it's already initialized with the right value
ConditionTree::ConditionTreeFactory.match_ids(@child_collection, [linked_id]).inverse
)
]
)
)

result = @child_collection.aggregate(@caller, old_fk_owner_filter, Aggregation.new(operation: 'Count'), 1)

return unless (result[0][:value]).positive?
return unless !(result[0]['value']).nil? && (result[0]['value']).positive?

# Avoids updating records to null if it's not authorized by the ORM
# and if there is no record to update (the filter returns no record)

@child_collection.update(@caller, old_fk_owner_filter, { relation.origin_key => nil })
end

def create_new_one_to_one_relationship(_scope, relation, origin_value, linked_id)
def create_new_one_to_one_relationship(relation, origin_value, linked_id)
return unless linked_id

new_fk_owner = ConditionTree::ConditionTreeFactory.match_ids(@child_collection, [linked_id])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def handle_request(args = {})
class_name: @collection.name,
is_collection: false,
serializer: Serializer::ForestSerializer,
include: projection.relations.keys
include: projection.relations(only_keys: true)
)
}
end
Expand Down
Loading

0 comments on commit 80566a5

Please sign in to comment.