diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c95d801de..7ccca33a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore index cb96cadd2..36e6fdeeb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,6 @@ vendor # GEM Gemfile.lock +Gemfile-test.lock *.gem pkg/ diff --git a/.releaserc.js b/.releaserc.js index e511c5f26..0da2fd0ca 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -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' ], diff --git a/.rubocop.yml b/.rubocop.yml index 44a6e26ce..5478a02ce 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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: @@ -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' @@ -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: diff --git a/packages/forest_admin_agent/Gemfile b/packages/forest_admin_agent/Gemfile index 6067bab87..19ee7bbd2 100644 --- a/packages/forest_admin_agent/Gemfile +++ b/packages/forest_admin_agent/Gemfile @@ -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' diff --git a/packages/forest_admin_agent/Gemfile-test b/packages/forest_admin_agent/Gemfile-test new file mode 100644 index 000000000..2685effd7 --- /dev/null +++ b/packages/forest_admin_agent/Gemfile-test @@ -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 diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb index c91c6ee10..2a01d0c02 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_authenticated_route.rb @@ -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 diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_related_route.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_related_route.rb index 2547275da..8241936eb 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_related_route.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/abstract_related_route.rb @@ -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 diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb index 3419268ee..acb5a1fbb 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/delete.rb @@ -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( [ diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb index 3b2222407..ee3bd1a3b 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb @@ -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) ) } diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb index df2870b8c..c1b2040aa 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/associate_related.rb @@ -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 } @@ -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) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb index 602c49f33..e6bcccb92 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/dissociate_related.rb @@ -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 @@ -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 diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb index 97e34b608..6baec4eb0 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/related/update_related.rb @@ -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', @@ -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 } @@ -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( [ @@ -67,18 +161,16 @@ 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) @@ -86,7 +178,7 @@ def break_old_one_to_one_relationship(_scope, relation, origin_value, linked_id) @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]) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb index 0206662c7..b9c5b8940 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/show.rb @@ -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 diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb index 503e69f63..9e1139839 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/store.rb @@ -20,10 +20,16 @@ def handle_request(args = {}) record = @collection.create(@caller, data) link_one_to_one_relations(args, record) + id = Utils::Id.unpack_id(@collection, record['id'], with_key: true) + filter = ForestAdminDatasourceToolkit::Components::Query::Filter.new( + condition_tree: ConditionTree::ConditionTreeFactory.match_records(@collection, [id]) + ) + records = @collection.list(@caller, filter, ProjectionFactory.all(@collection)) + { name: args[:params]['collection_name'], content: JSONAPI::Serializer.serialize( - record, + records[0], is_collection: false, class_name: @collection.name, serializer: Serializer::ForestSerializer @@ -34,7 +40,7 @@ def handle_request(args = {}) def link_one_to_one_relations(args, record) args[:params][:data][:relationships]&.map do |field, value| schema = @collection.schema[:fields][field] - next unless schema.type == 'OneToOne' + next unless schema.type == 'OneToOne' || schema.type == 'PolymorphicOneToOne' id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true) foreign_collection = @datasource.get_collection(schema.foreign_collection) @@ -42,9 +48,11 @@ def link_one_to_one_relations(args, record) origin_value = record[schema.origin_key_target] # update new relation (may update zero or one records). + patch = { schema.origin_key => origin_value } + patch[schema.origin_type_field] = @collection.name.gsub('__', '::') if schema.type == 'PolymorphicOneToOne' condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id]) filter = Filter.new(condition_tree: condition_tree) - foreign_collection.update(@caller, filter, { schema.origin_key => origin_value }) + foreign_collection.update(@caller, filter, patch) end end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb index af24fe9f4..c6d42d255 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer.rb @@ -21,11 +21,13 @@ def base_url def type class_name = @options[:class_name] - @@class_names[class_name] ||= class_name + @@class_names[class_name] ||= class_name.gsub('::', '__') end def id - forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(@options[:class_name]) + forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection( + @options[:class_name].gsub('::', '__') + ) primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(forest_collection) id = [] primary_keys.each { |key| id << @object[key] } @@ -50,7 +52,9 @@ def format_field(name, options) end def attributes - forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(@options[:class_name]) + forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection( + @options[:class_name].gsub('::', '__') + ) fields = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'Column' } fields.each { |field_name, _field| add_attribute(field_name) } return {} if attributes_map.nil? @@ -110,9 +114,13 @@ def add_to_many_association(name, options = {}, &block) def relationships datasource = ForestAdminAgent::Facades::Container.datasource - forest_collection = datasource.get_collection(@options[:class_name]) - relations_to_many = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'OneToMany' || field.type == 'ManyToMany' } - relations_to_one = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'OneToOne' || field.type == 'ManyToOne' } + forest_collection = datasource.get_collection(@options[:class_name].gsub('::', '__')) + relations_to_many = forest_collection.schema[:fields].select do |_field_name, field| + %w[OneToMany ManyToMany PolymorphicOneToMany].include?(field.type) + end + relations_to_one = forest_collection.schema[:fields].select do |_field_name, field| + %w[OneToOne ManyToOne PolymorphicManyToOne PolymorphicOneToOne].include?(field.type) + end relations_to_one.each { |field_name, _field| add_to_one_association(field_name) } @@ -133,14 +141,24 @@ def relationships if object.nil? || object.empty? data[formatted_attribute_name]['data'] = nil else - relation = datasource.get_collection(@options[:class_name]).schema[:fields][attribute_name.to_s] + relation = datasource.get_collection(@options[:class_name].gsub('::', '__')) + .schema[:fields][attribute_name.to_s] options = @options.clone - options[:class_name] = datasource.get_collection(relation.foreign_collection).name - related_object_serializer = ForestSerializer.new(object, options) - data[formatted_attribute_name]['data'] = { - 'type' => related_object_serializer.type.to_s, - 'id' => related_object_serializer.id.to_s, - } + if relation.type == 'PolymorphicManyToOne' + options[:class_name] = @object[relation.foreign_key_type_field] + related_object_serializer = ForestSerializer.new(object, options) + data[formatted_attribute_name]['data'] = { + 'type' => related_object_serializer.type.to_s, + 'id' => related_object_serializer.id.to_s, + } + else + options[:class_name] = datasource.get_collection(relation.foreign_collection).name + related_object_serializer = ForestSerializer.new(object, options) + data[formatted_attribute_name]['data'] = { + 'type' => related_object_serializer.type.to_s, + 'id' => related_object_serializer.id.to_s, + } + end end end @@ -161,7 +179,7 @@ def relationships if @_include_linkages.include?(formatted_attribute_name) || attr_data[:options][:include_data] data[formatted_attribute_name]['data'] = [] objects = has_many_relationship(attribute_name, attr_data) || [] - relation = datasource.get_collection(@options[:class_name]).schema[:fields][attribute_name.to_s] + relation = datasource.get_collection(@options[:class_name].gsub('::', '__')).schema[:fields][attribute_name.to_s] options = @options.clone options[:class_name] = datasource.get_collection(relation.foreign_collection).name objects.each do |obj| @@ -173,6 +191,7 @@ def relationships end end end + data end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer_override.rb b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer_override.rb index cac431839..b62467e78 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer_override.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/serializer/forest_serializer_override.rb @@ -25,6 +25,7 @@ def self.find_recursive_relationships(root_object, root_inclusion_tree, results, object = nil is_collection = false is_valid_attr = false + if serializer.has_one_relationships.key?(unformatted_attr_name) is_valid_attr = true attr_data = serializer.has_one_relationships[unformatted_attr_name] @@ -58,8 +59,15 @@ def self.find_recursive_relationships(root_object, root_inclusion_tree, results, # If it is not set, that indicates that this is an inner path and not a leaf and will # be followed by the recursion below. objects.each do |obj| - relation = ForestAdminAgent::Facades::Container.datasource.get_collection(options[:class_name]).schema[:fields][attribute_name] - relation_class_name = ForestAdminAgent::Facades::Container.datasource.get_collection(relation.foreign_collection).name + relation = ForestAdminAgent::Facades::Container.datasource + .get_collection(options[:class_name].gsub('::', '__')) + .schema[:fields][attribute_name] + if relation.type == 'PolymorphicManyToOne' + relation_class_name = root_object[relation.foreign_key_type_field] + else + relation_class_name = ForestAdminAgent::Facades::Container.datasource.get_collection(relation.foreign_collection).name + end + option_relation = options.clone option_relation[:class_name] = relation_class_name obj_serializer = JSONAPI::Serializer.find_serializer(obj, option_relation) @@ -203,6 +211,7 @@ def self.serialize(objects, options = {}) # of the internal special merging logic. find_recursive_relationships(obj, inclusion_tree, relationship_data, passthrough_options) end + result['included'] = relationship_data.map do |_, data| included_passthrough_options = {} included_passthrough_options[:base_url] = passthrough_options[:base_url] @@ -216,6 +225,7 @@ def self.serialize(objects, options = {}) serialize_primary(data[:object], included_passthrough_options) end end + result end end diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb index 512590f90..747f38078 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/query_string_parser.rb @@ -60,7 +60,13 @@ def self.parse_projection(collection, args) fields = fields.split(',').map do |field_name| column = collection.schema[:fields][field_name.strip] - column.type == 'Column' ? field_name.strip : "#{field_name.strip}:#{args[:params][:fields][field_name.strip]}" + if column.type == 'Column' + field_name.strip + elsif column.type == 'PolymorphicManyToOne' + "#{field_name.strip}:*" + else + "#{field_name.strip}:#{args[:params][:fields][field_name.strip]}" + end end Projection.new(fields) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb index 8f83a9e53..39243b650 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb @@ -4,20 +4,22 @@ module Schema class GeneratorField RELATION_MAP = { 'ManyToMany' => 'BelongsToMany', + 'PolymorphicManyToOne' => 'BelongsTo', 'ManyToOne' => 'BelongsTo', + 'PolymorphicOneToMany' => 'HasMany', 'OneToMany' => 'HasMany', + 'PolymorphicOneToOne' => 'HasOne', 'OneToOne' => 'HasOne' }.freeze def self.build_schema(collection, name) type = collection.schema[:fields][name].type - case type - when 'Column' - schema = build_column_schema(collection, name) - when 'ManyToOne', 'OneToMany', 'ManyToMany', 'OneToOne' - schema = build_relation_schema(collection, name) - end + schema = if type == 'Column' + build_column_schema(collection, name) + else + build_relation_schema(collection, name) + end schema.sort_by { |k, _v| k }.to_h end @@ -151,29 +153,53 @@ def build_many_to_one_schema(relation, collection, foreign_collection, base_sche ) end + def build_polymorphic_many_to_one_schema(relation, base_schema) + base_schema.merge( + { + type: 'Number', # default or take first foreign_key_targets ??, + defaultValue: nil, + isFilterable: false, + isPrimaryKey: false, + isRequired: false, + isReadOnly: false, + isSortable: false, + validations: [], + reference: "#{base_schema[:field]}.id", + polymorphic_referenced_models: relation.foreign_collections + } + ) + end + def build_relation_schema(collection, name) relation = collection.schema[:fields][name] - foreign_collection = collection.datasource.get_collection(relation.foreign_collection) - relation_schema = { field: name, enums: nil, integration: nil, isReadOnly: nil, isVirtual: false, - inverseOf: ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation(collection, name), relationship: RELATION_MAP[relation.type] } - case relation.type - when 'ManyToMany' - build_many_to_many_schema(relation, collection, foreign_collection, relation_schema) - when 'OneToMany' - build_one_to_many_schema(relation, collection, foreign_collection, relation_schema) - when 'OneToOne' - build_one_to_one_schema(relation, collection, foreign_collection, relation_schema) - when 'ManyToOne' - build_many_to_one_schema(relation, collection, foreign_collection, relation_schema) + if relation.type == 'PolymorphicManyToOne' + relation_schema[:inverseOf] = collection.name + build_polymorphic_many_to_one_schema(relation, relation_schema) + else + relation_schema[:inverseOf] = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation( + collection, + name + ) + foreign_collection = collection.datasource.get_collection(relation.foreign_collection) + case relation.type + when 'ManyToMany' + build_many_to_many_schema(relation, collection, foreign_collection, relation_schema) + when 'OneToMany', 'PolymorphicOneToMany' + build_one_to_many_schema(relation, collection, foreign_collection, relation_schema) + when 'OneToOne', 'PolymorphicOneToOne' + build_one_to_one_schema(relation, collection, foreign_collection, relation_schema) + when 'ManyToOne' + build_many_to_one_schema(relation, collection, foreign_collection, relation_schema) + end end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb index 2ed0aae66..ed3d299aa 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/associate_related_spec.rb @@ -50,6 +50,13 @@ module Related origin_key: 'user_id', origin_key_target: 'id', foreign_collection: 'address_user' + ), + 'addresses_poly' => Relations::PolymorphicOneToManySchema.new( + origin_key: 'addressable_id', + foreign_collection: 'address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'user' ) } } @@ -81,7 +88,15 @@ module Related schema: { fields: { 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), - 'location' => ColumnSchema.new(column_type: 'String') + 'location' => ColumnSchema.new(column_type: 'String'), + 'addressable_id' => ColumnSchema.new(column_type: 'Number'), + 'addressable_type' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['user'], + foreign_key_targets: { 'user' => 'id' }, + foreign_key: 'addressable_id' + ) } } ) @@ -150,6 +165,32 @@ module Related expect(result[:status]).to eq 204 end end + + context 'when call on polymorphic one to many relation' do + it 'call associate_polymorphic_one_to_many' do + args[:params]['relation_name'] = 'addresses_poly' + args[:params]['data'] = [{ 'id' => 1, 'type' => 'user' }] + args[:params]['id'] = 1 + allow(@datasource.get_collection('user')).to receive(:list).and_return([User.new(1, 'foo')]) + allow(@datasource.get_collection('address')).to receive(:update).and_return(true) + result = associate.handle_request(args) + + expect(@datasource.get_collection('address')).to have_received(:update) do |caller, filter, data| + expect(caller).to be_instance_of(Components::Caller) + expect(filter).to have_attributes( + condition_tree: have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + page: nil, + search: nil, + search_extended: nil, + segment: nil, + sort: nil + ) + expect(data).to eq({ 'addressable_id' => 1, 'addressable_type' => 'user' }) + end + expect(result[:content]).to be_nil + expect(result[:status]).to eq 204 + end + end end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb index f38d54929..1f07453ec 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/dissociate_related_spec.rb @@ -34,6 +34,13 @@ module Related origin_key: 'user_id', origin_key_target: 'id', foreign_collection: 'address_user' + ), + 'address_poly' => Relations::PolymorphicOneToManySchema.new( + origin_key: 'addressable_id', + foreign_collection: 'address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'user' ) } ) @@ -63,7 +70,15 @@ module Related { 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true, filter_operators: [Operators::IN, Operators::EQUAL]), - 'location' => ColumnSchema.new(column_type: 'String') + 'location' => ColumnSchema.new(column_type: 'String'), + 'addressable_id' => ColumnSchema.new(column_type: 'Number'), + 'addressable_type' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['user'], + foreign_key_targets: { 'user' => 'id' }, + foreign_key: 'addressable_id' + ) } ) @@ -302,6 +317,72 @@ module Related expect(result).to eq({ content: nil, status: 204 }) end + + context 'with PolymorphicOneToMany' do + it 'call dissociate_or_delete_one_to_many without deletion' do + allow(@datasource.get_collection('address')).to receive(:update).and_return(true) + + args[:params]['relation_name'] = 'address_poly' + args[:params][:data] = [{ 'id' => 1 }] + args[:params]['id'] = 1 + + result = dissociate.handle_request(args) + + expect(@datasource.get_collection('address')).to have_received(:update) do |caller, filter, data| + expect(caller).to be_instance_of(Components::Caller) + expect(filter).to have_attributes( + condition_tree: have_attributes( + aggregator: 'And', + conditions: [ + have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + have_attributes(field: 'addressable_id', operator: Operators::EQUAL, value: 1), + have_attributes(field: 'addressable_type', operator: Operators::EQUAL, value: 'user') + ] + ), + page: nil, + search: nil, + search_extended: nil, + segment: nil, + sort: nil + ) + expect(data).to eq({ 'addressable_id' => nil, 'addressable_type' => nil }) + end + + expect(result).to eq({ content: nil, status: 204 }) + end + + it 'call dissociate_or_delete_one_to_many with deletion' do + allow(@datasource.get_collection('address')).to receive(:delete).and_return(true) + + args[:params][:delete] = true + args[:params]['relation_name'] = 'address_poly' + args[:params][:data] = [{ 'id' => 1 }] + args[:params]['id'] = 1 + + result = dissociate.handle_request(args) + + expect(@datasource.get_collection('address')).to have_received(:delete) do |caller, filter| + expect(caller).to be_instance_of(Components::Caller) + expect(filter).to have_attributes( + condition_tree: have_attributes( + aggregator: 'And', + conditions: [ + have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + have_attributes(field: 'addressable_id', operator: Operators::EQUAL, value: 1), + have_attributes(field: 'addressable_type', operator: Operators::EQUAL, value: 'user') + ] + ), + page: nil, + search: nil, + search_extended: nil, + segment: nil, + sort: nil + ) + end + + expect(result).to eq({ content: nil, status: 204 }) + end + end end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb index efa1c8796..30dd08547 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/related/update_related_spec.rb @@ -26,6 +26,13 @@ module Related origin_key: 'author_id', origin_key_target: 'id', foreign_collection: 'book' + ), + 'address' => Relations::PolymorphicOneToOneSchema.new( + origin_key: 'addressable_id', + foreign_collection: 'address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'user' ) } ) @@ -44,9 +51,29 @@ module Related } ) + collection_address = collection_build( + name: 'address', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true, + filter_operators: [Operators::IN, Operators::EQUAL]), + 'location' => ColumnSchema.new(column_type: 'String'), + 'addressable_id' => ColumnSchema.new(column_type: 'Number'), + 'addressable_type' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['user'], + foreign_key_targets: { 'user' => 'id' }, + foreign_key: 'addressable_id' + ) + } + } + ) + allow(ForestAdminAgent::Builder::AgentFactory.instance).to receive(:send_schema).and_return(nil) datasource.add_collection(collection_user) datasource.add_collection(collection_book) + datasource.add_collection(collection_address) ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource) ForestAdminAgent::Builder::AgentFactory.instance.build @datasource = ForestAdminAgent::Facades::Container.datasource @@ -83,7 +110,7 @@ module Related args[:params]['collection_name'] = 'book' args[:params]['relation_name'] = 'author' - args[:params]['data'] = [{ 'id' => 1 }] + args[:params]['data'] = { 'id' => 1 } args[:params]['id'] = 1 result = update.handle_request(args) @@ -98,17 +125,42 @@ module Related segment: nil, sort: nil ) - expect(data).to eq({ 'author_id' => nil }) + expect(data).to eq({ 'author_id' => 1 }) + end + expect(result).to eq({ content: nil, status: 204 }) + end + + it 'call handle_request on a polymorphic_many_to_one relation' do + allow(@datasource.get_collection('address')).to receive(:update).and_return(true) + + args[:params]['collection_name'] = 'address' + args[:params]['relation_name'] = 'addressable' + args[:params]['data'] = { 'id' => 1, 'type' => 'user' } + args[:params]['id'] = 1 + + result = update.handle_request(args) + + expect(@datasource.get_collection('address')).to have_received(:update) do |caller, filter, data| + expect(caller).to be_instance_of(Components::Caller) + expect(filter).to have_attributes( + condition_tree: have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + page: nil, + search: nil, + search_extended: nil, + segment: nil, + sort: nil + ) + expect(data).to eq({ 'addressable_id' => 1, 'addressable_type' => 'user' }) end expect(result).to eq({ content: nil, status: 204 }) end it 'call handle_request on a one_to_one relation' do - allow(@datasource.get_collection('book')).to receive_messages(aggregate: [{ value: 1 }], update: true) + allow(@datasource.get_collection('book')).to receive_messages(aggregate: [{ 'value' => 1 }], update: true) args[:params]['collection_name'] = 'user' args[:params]['relation_name'] = 'book' - args[:params][:data] = { id: 1 } + args[:params]['data'] = { 'id' => 1 } args[:params]['id'] = 1 result = update.handle_request(args) @@ -155,6 +207,60 @@ module Related end expect(result).to eq({ content: nil, status: 204 }) end + + it 'call handle_request on a polymorphic_one_to_one relation' do + allow(@datasource.get_collection('address')).to receive_messages(aggregate: [{ 'value' => 1 }], update: true) + + args[:params]['collection_name'] = 'user' + args[:params]['relation_name'] = 'address' + args[:params]['data'] = { 'id' => 1, 'type' => 'user ' } + args[:params]['id'] = 1 + + result = update.handle_request(args) + + parameters = [ + [ + Components::Caller, + { + condition_tree: have_attributes( + aggregator: 'And', + conditions: [ + have_attributes(field: 'addressable_id', operator: Operators::EQUAL, value: 1), + have_attributes(field: 'addressable_type', operator: Operators::EQUAL, value: 'user'), + have_attributes(field: 'id', operator: Operators::NOT_EQUAL, value: 1) + ] + ), + page: nil, + search: nil, + search_extended: nil, + segment: nil, + sort: nil + }, + { 'addressable_id' => nil, 'addressable_type' => nil } + ], + [ + Components::Caller, + { + condition_tree: have_attributes(field: 'id', operator: Operators::EQUAL, value: 1), + page: nil, + search: nil, + search_extended: nil, + segment: nil, + sort: nil + }, + { 'addressable_id' => 1, 'addressable_type' => 'user' } + ] + ] + + expect(@datasource.get_collection('address')).to have_received(:update) + .exactly(2).times do |caller, filter, data| + parameter = parameters.shift + expect(caller).to be_instance_of(parameter[0]) + expect(filter).to have_attributes(parameter[1]) + expect(data).to eq(parameter[2]) + end + expect(result).to eq({ content: nil, status: 204 }) + end end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb index 5b4bbbe70..28b36922f 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/store_spec.rb @@ -70,7 +70,8 @@ def respond_to?(arg) 'price' => ColumnSchema.new(column_type: 'Number') } }, - create: book + create: book, + list: [book] ) datasource.add_collection(collection) @@ -156,8 +157,9 @@ def respond_to?(arg) type: 'persons' } args[:params]['collection_name'] = 'person' - allow(@datasource.get_collection('person')).to receive(:create).and_return( - { 'id' => 1, 'name' => 'john' } + allow(@datasource.get_collection('person')).to receive_messages( + create: { 'id' => 1, 'name' => 'john' }, + list: [{ 'id' => 1, 'name' => 'john' }] ) allow(@datasource.get_collection('passport')).to receive(:update).and_return( { 'id' => 1, 'person_id' => 1 } @@ -200,8 +202,9 @@ def respond_to?(arg) type: 'persons' } args[:params]['collection_name'] = 'passport' - allow(@datasource.get_collection('passport')).to receive(:create).and_return( - { 'id' => 1, 'person_id' => 1 } + allow(@datasource.get_collection('passport')).to receive_messages( + create: { 'id' => 1, 'person_id' => 1 }, + list: [{ 'id' => 1, 'person_id' => 1 }] ) result = store.handle_request(args) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/query_string_parser_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/query_string_parser_spec.rb index 5ae3bbd5f..ae98528e7 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/query_string_parser_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/query_string_parser_spec.rb @@ -75,44 +75,94 @@ module Utils end describe 'parse_projection' do - let(:collection) do - datasource = Datasource.new - collection_person = Collection.new(datasource, 'Person') - collection_person.add_fields( - { - 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), - 'books' => Relations::ManyToManySchema.new( - origin_key: 'person_id', - origin_key_target: 'id', - foreign_collection: 'Book', - foreign_key: 'book_id', - foreign_key_target: 'id', - through_collection: 'BookPerson' - ) - } - ) - collection = Collection.new(datasource, 'Book') - collection.add_fields( - { - 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), - 'composite_id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), - 'title' => ColumnSchema.new(column_type: 'String'), - 'author_id' => ColumnSchema.new(column_type: 'Number'), - 'author' => Relations::ManyToOneSchema.new( - foreign_key: 'author_id', - foreign_key_target: 'id', - foreign_collection: 'Person' - ) - } - ) + context 'when collection has PolymorphicManyToOne' do + let(:collection) do + datasource = Datasource.new + collection = Collection.new(datasource, 'Address') + collection.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'addressable_id' => ColumnSchema.new(column_type: 'Number'), + 'addressable_type' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['User'], + foreign_key_targets: { 'User' => 'id' }, + foreign_key: 'addressable_id' + ) + } + ) + collection_user = Collection.new(datasource, 'User') + collection_user.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'email' => ColumnSchema.new(column_type: 'String'), + 'address' => Relations::PolymorphicOneToOneSchema.new( + origin_key: 'addressable_id', + foreign_collection: 'address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'User' + ) + } + ) - datasource.add_collection(collection) - datasource.add_collection(collection_person) + datasource.add_collection(collection) + datasource.add_collection(collection_user) - return collection + return collection + end + + it 'convert the request to a valid projection with polymorphic_relation:*' do + args = { + params: { + fields: { 'Address' => 'id,addressable,addressable_id,addressable_type' } + } + } + expect(described_class.parse_projection(collection, args)).to eq( + Projection.new(%w[id addressable:* addressable_id addressable_type]) + ) + end end context 'when request is well formed' do + let(:collection) do + datasource = Datasource.new + collection_person = Collection.new(datasource, 'Person') + collection_person.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'books' => Relations::ManyToManySchema.new( + origin_key: 'person_id', + origin_key_target: 'id', + foreign_collection: 'Book', + foreign_key: 'book_id', + foreign_key_target: 'id', + through_collection: 'BookPerson' + ) + } + ) + collection = Collection.new(datasource, 'Book') + collection.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'composite_id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'title' => ColumnSchema.new(column_type: 'String'), + 'author_id' => ColumnSchema.new(column_type: 'Number'), + 'author' => Relations::ManyToOneSchema.new( + foreign_key: 'author_id', + foreign_key_target: 'id', + foreign_collection: 'Person' + ) + } + ) + + datasource.add_collection(collection) + datasource.add_collection(collection_person) + + return collection + end + it 'convert the request to a valid projection' do args = { params: { diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_polymorphic_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_polymorphic_spec.rb new file mode 100644 index 000000000..9c263e165 --- /dev/null +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_polymorphic_spec.rb @@ -0,0 +1,149 @@ +require 'spec_helper' + +module ForestAdminAgent + module Utils + module Schema + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Schema + describe GeneratorField do + context 'when field is polymorphic relation' do + before do + collection_address = collection_build( + name: 'Address', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'addressable_id' => ColumnSchema.new(column_type: 'Number'), + 'addressable_type' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: %w[User Order], + foreign_key_targets: { 'User' => 'id', 'Order' => 'id' }, + foreign_key: 'addressable_id' + ) + } + } + ) + + collection_user = collection_build( + name: 'User', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'email' => ColumnSchema.new(column_type: 'String'), + 'addresses' => Relations::PolymorphicOneToManySchema.new( + origin_key: 'addressable_id', + foreign_collection: 'Address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'User' + ) + } + } + ) + + collection_order = collection_build( + name: 'Order', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'reference' => ColumnSchema.new(column_type: 'String'), + 'address' => Relations::PolymorphicOneToOneSchema.new( + origin_key: 'addressable_id', + foreign_collection: 'Address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'Order' + ) + } + } + ) + + @datasource = datasource_with_collections_build( + [collection_address, collection_order, collection_user] + ) + end + + describe 'with a PolymorphicManyToOne' do + it 'generate the relation' do + schema = described_class.build_schema(@datasource.get_collection('Address'), 'addressable') + + expect(schema).to match( + { + field: 'addressable', + inverseOf: 'Address', + reference: 'addressable.id', + relationship: 'BelongsTo', + type: 'Number', + defaultValue: nil, + enums: nil, + integration: nil, + isFilterable: false, + isPrimaryKey: false, + isReadOnly: false, + isRequired: false, + isSortable: false, + isVirtual: false, + validations: [], + polymorphic_referenced_models: %w[User Order] + } + ) + end + end + + describe 'with a PolymorphicOneToOne' do + it 'generate the relation' do + schema = described_class.build_schema(@datasource.get_collection('Order'), 'address') + + expect(schema).to match( + { + field: 'address', + inverseOf: 'addressable', + reference: 'Address.id', + relationship: 'HasOne', + type: 'Number', + defaultValue: nil, + enums: nil, + integration: nil, + isFilterable: false, + isPrimaryKey: false, + isReadOnly: false, + isRequired: false, + isSortable: false, + isVirtual: false, + validations: [] + } + ) + end + end + + describe 'with a PolymorphicOneToMany' do + it 'generate the relation' do + schema = described_class.build_schema(@datasource.get_collection('User'), 'addresses') + + expect(schema).to match( + { + field: 'addresses', + inverseOf: 'addressable', + reference: 'Address.id', + relationship: 'HasMany', + type: ['Number'], + defaultValue: nil, + enums: nil, + integration: nil, + isFilterable: false, + isPrimaryKey: false, + isReadOnly: false, + isRequired: false, + isSortable: true, + isVirtual: false, + validations: [] + } + ) + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_active_record/Gemfile b/packages/forest_admin_datasource_active_record/Gemfile index 3e6a58258..d3cfa3f4b 100644 --- a/packages/forest_admin_datasource_active_record/Gemfile +++ b/packages/forest_admin_datasource_active_record/Gemfile @@ -3,14 +3,12 @@ source "https://rubygems.org" # Specify your gem's dependencies in forest_admin_datasource_active_record.gemspec gemspec +gem 'database_cleaner-active_record' gem 'forest_admin_datasource_toolkit' - gem 'rake', '~> 13.0' - gem 'rubocop', '~> 1.21' group :development, :test do - gem 'database_cleaner-active_record' gem 'rails' gem 'rspec-rails', '~> 3.0' gem 'simplecov', '~> 0.22', require: false diff --git a/packages/forest_admin_datasource_active_record/Gemfile-test b/packages/forest_admin_datasource_active_record/Gemfile-test new file mode 100644 index 000000000..b9c9ef8bd --- /dev/null +++ b/packages/forest_admin_datasource_active_record/Gemfile-test @@ -0,0 +1,18 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in forest_admin_datasource_active_record.gemspec +gemspec + +gem 'rake', '~> 13.0' +gem 'rubocop', '~> 1.21' + +group :development, :test do + gem 'database_cleaner-active_record' + gem 'forest_admin_datasource_toolkit', path: '../forest_admin_datasource_toolkit' + gem 'rails' + gem 'rspec-rails', '~> 3.0' + gem 'simplecov', '~> 0.22', require: false + gem 'simplecov-html', '~> 0.12.3' + gem 'simplecov_json_formatter', '~> 0.1.4' + gem 'sqlite3', '< 2.0' +end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index 2b99e0ef2..da941ee59 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -6,9 +6,10 @@ class Collection < ForestAdminDatasourceToolkit::Collection attr_reader :model - def initialize(datasource, model) + def initialize(datasource, model, support_polymorphic_relations: false) @model = model - name = model.name.demodulize.underscore + @support_polymorphic_relations = support_polymorphic_relations + name = format_model_name(@model.name) super(datasource, name) fetch_fields fetch_associations @@ -35,7 +36,7 @@ def create(_caller, data) def update(_caller, filter, data) entity = Utils::Query.new(self, nil, filter).build.first - entity.update(data) + entity&.update(data) end def delete(_caller, filter) @@ -45,6 +46,10 @@ def delete(_caller, filter) private + def format_model_name(class_name) + class_name.gsub('::', '__') + end + def fetch_fields @model.columns_hash.each do |column_name, column| field = ForestAdminDatasourceToolkit::Schema::ColumnSchema.new( @@ -66,11 +71,11 @@ def fetch_fields def association_primary_key?(association) !association.association_primary_key.empty? rescue StandardError - false + association.polymorphic? end def fetch_associations - associations(@model).each do |association| + associations(@model, support_polymorphic_relations: @support_polymorphic_relations).each do |association| case association.macro when :has_one if association_primary_key?(association) @@ -78,19 +83,30 @@ def fetch_associations add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new( - foreign_collection: association.class_name.demodulize.underscore, + foreign_collection: format_model_name(association.klass.name), origin_key: association.through_reflection.foreign_key, origin_key_target: association.through_reflection.join_foreign_key, foreign_key: association.join_foreign_key, foreign_key_target: association.association_primary_key, - through_collection: association.through_reflection.class_name.demodulize.underscore + through_collection: format_model_name(association.through_reflection.klass.name) + ) + ) + elsif association.inverse_of.polymorphic? + add_field( + association.name.to_s, + ForestAdminDatasourceToolkit::Schema::Relations::PolymorphicOneToOneSchema.new( + foreign_collection: format_model_name(association.klass.name), + origin_key: association.foreign_key, + origin_key_target: association.association_primary_key, + origin_type_field: association.inverse_of.foreign_type, + origin_type_value: @model.name ) ) else add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::OneToOneSchema.new( - foreign_collection: association.class_name.demodulize.underscore, + foreign_collection: format_model_name(association.klass.name), origin_key: association.foreign_key, origin_key_target: association.association_primary_key ) @@ -99,14 +115,29 @@ def fetch_associations end when :belongs_to if association_primary_key?(association) - add_field( - association.name.to_s, - ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema.new( - foreign_collection: association.class_name.demodulize.underscore, - foreign_key: association.foreign_key, - foreign_key_target: association.association_primary_key + if polymorphic?(association) + foreign_collections = get_polymorphic_types(association) + add_field( + association.name.to_s, + ForestAdminDatasourceToolkit::Schema::Relations::PolymorphicManyToOneSchema.new( + foreign_collections: foreign_collections.keys, + foreign_key: association.foreign_key, + foreign_key_type_field: association.foreign_type, + foreign_key_targets: foreign_collections + ) ) - ) + schema[:fields][association.foreign_key].is_read_only = true + schema[:fields][association.foreign_type].is_read_only = true + else + add_field( + association.name.to_s, + ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema.new( + foreign_collection: format_model_name(association.klass.name), + foreign_key: association.foreign_key, + foreign_key_target: association.association_primary_key + ) + ) + end end when :has_many if association_primary_key?(association) @@ -114,19 +145,30 @@ def fetch_associations add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema.new( - foreign_collection: association.class_name.demodulize.underscore, + foreign_collection: format_model_name(association.klass.name), origin_key: association.through_reflection.foreign_key, origin_key_target: association.through_reflection.join_foreign_key, foreign_key: association.join_foreign_key, foreign_key_target: association.association_primary_key, - through_collection: association.through_reflection.class_name.demodulize.underscore + through_collection: format_model_name(association.through_reflection.klass.name) + ) + ) + elsif association.inverse_of.polymorphic? + add_field( + association.name.to_s, + ForestAdminDatasourceToolkit::Schema::Relations::PolymorphicOneToManySchema.new( + foreign_collection: format_model_name(association.klass.name), + origin_key: association.foreign_key, + origin_key_target: association.association_primary_key, + origin_type_field: association.inverse_of.foreign_type, + origin_type_value: @model.name ) ) else add_field( association.name.to_s, ForestAdminDatasourceToolkit::Schema::Relations::OneToManySchema.new( - foreign_collection: association.class_name.demodulize.underscore, + foreign_collection: format_model_name(association.klass.name), origin_key: association.foreign_key, origin_key_target: association.association_primary_key ) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb index c7bebc15c..318f1ef8b 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb @@ -2,9 +2,12 @@ module ForestAdminDatasourceActiveRecord class Datasource < ForestAdminDatasourceToolkit::Datasource - def initialize(db_config = {}) + attr_reader :models + + def initialize(db_config = {}, support_polymorphic_relations: false) super() @models = [] + @support_polymorphic_relations = support_polymorphic_relations @habtm_models = {} init_orm(db_config) generate @@ -15,7 +18,9 @@ def initialize(db_config = {}) def generate ActiveRecord::Base.descendants.each { |model| fetch_model(model) } @models.each do |model| - add_collection(Collection.new(self, model)) + add_collection( + Collection.new(self, model, support_polymorphic_relations: @support_polymorphic_relations) + ) end end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/relation.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/relation.rb index 2682a5932..68266da7b 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/relation.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/relation.rb @@ -1,14 +1,19 @@ module ForestAdminDatasourceActiveRecord module Parser module Relation - def associations(model) + def associations(model, support_polymorphic_relations: false) model.reflect_on_all_associations.select do |association| - !polymorphic?(association) && !active_type?(association.klass) + if support_polymorphic_relations + polymorphic?(association) ? true : !active_type?(association.klass) + else + !polymorphic?(association) && !active_type?(association.klass) + end end end def polymorphic?(association) - association.options[:polymorphic] + (association.options.key?(:polymorphic) && association.options[:polymorphic]) || + association.inverse_of&.polymorphic? end # NOTICE: Ignores ActiveType::Object association during introspection and interactions. @@ -16,6 +21,17 @@ def polymorphic?(association) def active_type?(model) Object.const_defined?('ActiveType::Object') && model < ActiveType::Object end + + def get_polymorphic_types(relation) + types = {} + @datasource.models.each do |model| + unless model.reflect_on_all_associations.none? { |assoc| assoc.options[:as] == relation.name.to_sym } + types[format_model_name(model.name)] = model.primary_key + end + end + + types + end end end end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/active_record_serializer.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/active_record_serializer.rb index cf2558b00..68bf02631 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/active_record_serializer.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/active_record_serializer.rb @@ -13,7 +13,7 @@ def hash_object(object, projection = nil, with_associations: true) hash.merge! object.attributes if with_associations - each_association_collection(object) do |association_name, item| + each_association_collection(object, projection) do |association_name, item| hash[association_name] = hash_object( item, projection.relations[association_name], @@ -25,9 +25,10 @@ def hash_object(object, projection = nil, with_associations: true) hash end - def each_association_collection(object) + def each_association_collection(object, projection) one_associations = %i[has_one belongs_to] - object.class.reflect_on_all_associations.filter { |a| one_associations.include?(a.macro) } + object.class.reflect_on_all_associations + .filter { |a| one_associations.include?(a.macro) && projection.relations.key?(a.name.to_s) } .each { |association| yield(association.name.to_s, object.send(association.name.to_s)) } end end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 1de8c15ba..afbb826f3 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -123,14 +123,11 @@ def add_join_relation(relation_name) end def format_field(field) - @select << "#{@collection.model.table_name}.#{field}" - if field.include?(':') relation_name, field = field.split(':') relation = @collection.schema[:fields][relation_name] table_name = @collection.datasource.get_collection(relation.foreign_collection).model.table_name add_join_relation(relation_name) - @select << "#{table_name}.#{field}" return "#{table_name}.#{field}" end diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb index 75989555e..e02dcefc1 100644 --- a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb +++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb @@ -2,32 +2,55 @@ module ForestAdminDatasourceActiveRecord describe Collection do - let(:collection) do - datasource = Datasource.new(adapter: 'sqlite3', database: 'db/database.db') - described_class.new(datasource, Car) - end + context 'without polymorphic support' do + let(:datasource) { Datasource.new({ adapter: 'sqlite3', database: 'db/database.db' }) } + let(:collection) do + described_class.new(datasource, Car) + end + + describe 'fetch_fields' do + it 'add all fields of model to the collection' do + expect(collection.schema[:fields].keys).to include( + 'id', + 'category_id', + 'reference', + 'model', + 'brand', + 'year', + 'nb_seats', + 'is_manual', + 'options', + 'created_at', + 'updated_at' + ) + end + end - describe 'fetch_fields' do - it 'add all fields of model to the collection' do - expect(collection.schema[:fields].keys).to include( - 'id', - 'category_id', - 'reference', - 'model', - 'brand', - 'year', - 'nb_seats', - 'is_manual', - 'options', - 'created_at', - 'updated_at' - ) + describe 'fetch_associations' do + it 'add all relation of model to the collection' do + expect(collection.schema[:fields].keys).to include('category', 'user', 'car_checks', 'checks') + end + + it 'do not add polymorphic relations' do + expect(datasource.get_collection('User').schema[:fields].keys).not_to include('address') + expect(datasource.get_collection('Address').schema[:fields].keys).not_to include('addressable') + end end end - describe 'fetch_associations' do - it 'add all relation of model to the collection' do - expect(collection.schema[:fields].keys).to include('category', 'user', 'car_checks', 'checks') + context 'with polymorphic support' do + let(:datasource) do + Datasource.new({ adapter: 'sqlite3', database: 'db/database.db' }, support_polymorphic_relations: true) + end + let(:collection) do + described_class.new(datasource, Car) + end + + describe 'fetch_associations' do + it 'add polymorphic relations' do + expect(datasource.get_collection('User').schema[:fields].keys).to include('address') + expect(datasource.get_collection('Address').schema[:fields].keys).to include('addressable') + end end end end diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb index 05ef5b3bd..ef583c150 100644 --- a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb +++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb @@ -3,8 +3,8 @@ module ForestAdminDatasourceActiveRecord describe Datasource do it 'fetch all models' do - datasource = described_class.new(adapter: 'sqlite3', database: 'db/database.db') - expected = %w[user order check category car_check car address] + datasource = described_class.new({ adapter: 'sqlite3', database: 'db/database.db' }) + expected = %w[User Order Check Category CarCheck Car Address] expect(datasource.collections.keys).to match_array(expected) end diff --git a/packages/forest_admin_datasource_customizer/Gemfile b/packages/forest_admin_datasource_customizer/Gemfile index 915f406db..dabaf008d 100644 --- a/packages/forest_admin_datasource_customizer/Gemfile +++ b/packages/forest_admin_datasource_customizer/Gemfile @@ -1,11 +1,9 @@ source "https://rubygems.org" -# Specify your gem's dependencies in forest_admin_datasource_toolkit.gemspec gemspec gem 'forest_admin_agent' gem 'forest_admin_datasource_toolkit' - gem 'rake', '~> 13.0' gem 'rubocop', '~> 1.21' diff --git a/packages/forest_admin_datasource_customizer/Gemfile-test b/packages/forest_admin_datasource_customizer/Gemfile-test new file mode 100644 index 000000000..679ef6bd2 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/Gemfile-test @@ -0,0 +1,15 @@ +source "https://rubygems.org" + +gemspec + +gem 'rake', '~> 13.0' +gem 'rubocop', '~> 1.21' + +group :development, :test do + gem 'forest_admin_agent', path: '../forest_admin_agent' + 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 diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb index 3f9cf9564..d600850ac 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb @@ -134,6 +134,8 @@ def convert_value(to_backend, path, value) prefix, suffix = path.split(':') field = @child_collection.schema[:fields][prefix] + return value if field.type == 'PolymorphicManyToOne' + if field.type != 'Column' foreign_collection = @datasource.get_collection(field.foreign_collection) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb index 81f145039..0a9f610be 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb @@ -16,6 +16,15 @@ def get_computed(path) index = path.index(':') return @computeds[path] if index.nil? + if schema[:fields][path[0, index]].type == 'PolymorphicManyToOne' + ForestAdminAgent::Facades::Container.logger.log( + 'Debug', + "Cannot compute field over polymorphic relation #{name}.#{path[0, index]}." + ) + + return @computeds[path] + end + foreign_collection = schema[:fields][path[0, index]].foreign_collection association = @datasource.get_collection(foreign_collection) @@ -27,6 +36,10 @@ def register_computed(name, computed) # Check that all dependencies exist and are columns computed.dependencies.each do |field| + if field.include?(':') && schema[:fields][field.partition(':')[0]].type == 'PolymorphicManyToOne' + raise ForestException, + "Dependencies over a polymorphic relations(#{self.name}.#{field.partition(":")[0]}) are forbidden" + end FieldValidator.validate(self, field) end @@ -90,12 +103,14 @@ def rewrite_field(collection, path) if path.include?(':') prefix = path.split(':')[0] schema = collection.schema[:fields][prefix] - association = collection.datasource.get_collection(schema.foreign_collection) + if schema.type != 'PolymorphicManyToOne' + association = collection.datasource.get_collection(schema.foreign_collection) - return Projection.new([path]) - .unnest - .replace { |sub_path| rewrite_field(association, sub_path) } - .nest(prefix: prefix) + return Projection.new([path]) + .unnest + .replace { |sub_path| rewrite_field(association, sub_path) } + .nest(prefix: prefix) + end end # Computed field that we own: recursively replace by dependencies diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb index f415a13f8..b8bd002f1 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb @@ -11,7 +11,8 @@ def refine_schema(sub_schema) schema = sub_schema.dup schema[:fields] = sub_schema[:fields].dup - schema[:fields].map do |_name, field_schema| + schema[:fields].each do |name, field_schema| + field_schema = field_schema.dup if field_schema.type == 'Column' new_operators = Operators.all.select do |operator| ConditionTreeEquivalent.equivalent_tree?(operator, field_schema.filter_operators, @@ -19,9 +20,9 @@ def refine_schema(sub_schema) end field_schema.filter_operators = new_operators - else - field_schema end + + schema[:fields][name] = field_schema end schema diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb index 4f2beba26..8685b1417 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb @@ -17,6 +17,13 @@ def change_field_visibility(name, visible) child_collection, name ) + @child_collection.schema[:fields].each do |field_name, field_schema| + next unless field_schema.type == 'PolymorphicManyToOne' && + [field_schema.foreign_key, field_schema.foreign_key_type_field].include?(name) + + raise ForestException, "Cannot remove field '#{self.name}.#{name}', because it's implied " \ + "in a polymorphic relation '#{self.name}.#{field_name}'" + end if visible @blacklist.delete(name) else @@ -37,14 +44,15 @@ def create(caller, data) def refine_schema(child_schema) fields = {} + schema = child_schema.dup - child_schema[:fields].each do |name, field| + schema[:fields].each do |name, field| fields[name] = field if published?(name) end - child_schema[:fields] = fields + schema[:fields] = fields - child_schema + schema end def published?(name) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb index 272fe5acd..6051b80b3 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb @@ -33,6 +33,7 @@ def keep_collections_matching(include = [], exclude = []) def remove_collection(collection_name) validate_collection_names([collection_name]) + validate_is_removable(collection_name) # Delete the collection @blacklist << collection_name @@ -48,6 +49,17 @@ def published?(collection_name) private + def validate_is_removable(collection_name) + collection = get_collection(collection_name) + collection.schema[:fields].each do |field_name, field_schema| + next unless field_schema.type == 'PolymorphicOneToOne' || field_schema.type == 'PolymorphicOneToMany' + + inverse = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation(collection, field_name) + + raise ForestException, "Cannot remove #{collection.name} because it's a potential target of polymorphic relation #{field_schema.foreign_collection}.#{inverse}" + end + end + def validate_collection_names(names) names.each { |name| get_collection(name) } end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb index c41f9d606..480a919c2 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb @@ -177,7 +177,7 @@ def rewrite_field(field) prefix = field.split(':').first field_schema = schema[:fields][prefix] - return [field] if field_schema.type == 'Column' + return [field] if field_schema.type == 'Column' || field_schema.type == 'PolymorphicManyToOne' relation = datasource.get_collection(field_schema.foreign_collection) result = [] diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb index e760f2d7e..4b0343a33 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb @@ -55,6 +55,16 @@ def rename_collection(current_name, new_name) "Cannot rename a collection twice: #{@to_child_name[current_name]}->#{current_name}->#{new_name}" end + get_collection(current_name).schema[:fields].each do |field_name, field_schema| + next unless field_schema.type == 'PolymorphicOneToOne' || field_schema.type == 'PolymorphicOneToMany' + + reverse_relation_name = Utils::Collection.get_inverse_relation(get_collection(current_name), field_name) + + raise Exceptions::ForestException, + "Cannot rename collection #{current_name} because it's a target of a polymorphic relation " \ + "'#{field_schema.foreign_collection}.#{reverse_relation_name}'" + end + @from_child_name[current_name] = new_name @to_child_name[new_name] = current_name diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb index 2ebca46f5..fce475742 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb @@ -13,7 +13,7 @@ def refine_schema(sub_schema) fields = {} sub_schema[:fields].each do |name, old_schema| - if old_schema.type != 'Column' + if old_schema.type != 'Column' && old_schema.type != 'PolymorphicManyToOne' old_schema.foreign_collection = datasource.get_collection_name(old_schema.foreign_collection) if old_schema.type == 'ManyToMany' old_schema.through_collection = datasource.get_collection_name(old_schema.through_collection) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb index f405c7cd6..74e331f94 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb @@ -3,6 +3,7 @@ module Decorators module RenameField class RenameFieldCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator include ForestAdminDatasourceToolkit::Decorators + include ForestAdminDatasourceToolkit::Exceptions include ForestAdminDatasourceToolkit::Components::Query::ConditionTree attr_accessor :from_child_collection, :to_child_collection @@ -22,6 +23,14 @@ def rename_field(current_name, new_name) ForestAdminDatasourceToolkit::Validations::FieldValidator.validate_name(name, new_name) + @child_collection.schema[:fields].each do |field_name, field_schema| + next unless field_schema.type == 'PolymorphicManyToOne' && + [field_schema.foreign_key, field_schema.foreign_key_type_field].include?(current_name) + + raise ForestException, "Cannot rename '#{name}.#{current_name}', because it's implied " \ + "in a polymorphic relation '#{name}.#{field_name}'" + end + # Revert previous renaming (avoids conflicts and need to recurse on @to_child_collection). if to_child_collection[current_name] child_name = to_child_collection[current_name] @@ -41,6 +50,7 @@ def rename_field(current_name, new_name) def refine_schema(sub_schema) fields = {} + schema = sub_schema.dup sub_schema[:fields].each do |old_name, old_schema| case old_schema.type @@ -58,9 +68,9 @@ def refine_schema(sub_schema) fields[from_child_collection[old_name] || old_name] = old_schema end - sub_schema[:fields] = fields + schema[:fields] = fields - sub_schema + schema end def refine_filter(_caller, filter = nil) @@ -89,7 +99,7 @@ def create(caller, data) def list(caller, filter, projection) child_projection = projection.replace { |field| path_to_child_collection(field) } records = @child_collection.list(caller, filter, child_projection) - return records if child_projection == projection + return records if child_projection.sort == projection.sort records.map { |record| record_from_child_collection(record) } end @@ -134,9 +144,11 @@ def path_from_child_collection(path) child_field = paths[0] relation_name = from_child_collection[child_field] || child_field relation_schema = schema[:fields][relation_name] - relation = datasource.get_collection(relation_schema.foreign_collection) + if relation_schema.type != 'PolymorphicManyToOne' + relation = datasource.get_collection(relation_schema.foreign_collection) - return "#{relation_name}:#{relation.path_from_child_collection(paths[1])}" + return "#{relation_name}:#{relation.path_from_child_collection(paths[1])}" + end end from_child_collection[path] ||= path @@ -148,10 +160,12 @@ def path_to_child_collection(path) paths = path.split(':') relation_name = paths[0] relation_schema = schema[:fields][relation_name] - relation = datasource.get_collection(relation_schema.foreign_collection) - child_field = to_child_collection[relation_name] || relation_name + if relation_schema.type != 'PolymorphicManyToOne' + relation = datasource.get_collection(relation_schema.foreign_collection) + child_field = to_child_collection[relation_name] || relation_name - return "#{child_field}:#{relation.path_to_child_collection(paths[1])}" + return "#{child_field}:#{relation.path_to_child_collection(paths[1])}" + end end to_child_collection[path] ||= path @@ -173,8 +187,8 @@ def record_from_child_collection(child_record) field = from_child_collection[child_field] || child_field field_schema = schema[:fields][field] - # Perform the mapping, recurse for relations - if field_schema.type == 'Column' || value.nil? + # Perform the mapping, recurse for relation + if field_schema.type == 'Column' || field_schema.type == 'PolymorphicManyToOne' || value.nil? record[field] = value else relation = datasource.get_collection(field_schema.foreign_collection) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb index aec509924..ca60fe2a3 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb @@ -104,7 +104,17 @@ def get_fields(collection, extended) collection.schema[:fields].each do |name, field| fields.push([name, field]) if field.type == 'Column' - next unless extended && (field.type == 'ManyToOne' || field.type == 'OneToOne') + if field.type == 'PolymorphicManyToOne' + ForestAdminAgent::Facades::Container.logger.log( + 'Debug', + "We're not searching through #{self.name}.#{name} because it's a polymorphic relation. " \ + "You can override the default search behavior with 'replace_search'. " \ + 'See more: https://docs.forestadmin.com/developer-guide-agents-ruby/agent-customization/search' + ) + end + + next unless extended && + (field.type == 'ManyToOne' || field.type == 'OneToOne' || field.type == 'PolymorphicOneToOne') related = collection.datasource.get_collection(field.foreign_collection) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb index c256a1a24..ddbcbd0d6 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb @@ -72,10 +72,10 @@ def rewrite_key(context, key, used) if field_schema&.type == 'Column' # We either call the customer handler or a default one that does nothing. handler = @handlers[key] || proc { |v| { key => v } } - field_patch = if context.record[key] && handler.call(context.record[key], context) + field_patch = if context.record.key?(key) && handler.call(context.record[key], context) handler.call(context.record[key], context) else - [] + {} end if field_patch && !field_patch.is_a?(Hash) @@ -84,10 +84,17 @@ def rewrite_key(context, key, used) # Isolate change to our own value (which should not recurse) and the rest which should # trigger the other handlers. - value = field_patch[key] || nil + if field_patch.key?(key) + value = field_patch[key] + is_value = true + else + value = nil + is_value = false + end + new_patch = rewrite_patch(context.caller, context.action, field_patch.except(key), used + [key]) - value ? deep_merge({ key => value }, new_patch) : new_patch + is_value ? deep_merge({ key => value }, new_patch) : new_patch elsif field_schema&.type == 'ManyToOne' || field_schema&.type == 'OneToOne' # Delegate relations to the appropriate collection. relation = datasource.get_collection(field_schema.foreign_collection) diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb index f4bf35b6e..b95917ed8 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/datasource_customizer_spec.rb @@ -13,7 +13,12 @@ module ForestAdminDatasourceCustomizer schema: { fields: { 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true) - } + }, + countable: false, + searchable: false, + charts: [], + segments: {}, + actions: {} } ) diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator_spec.rb index 541b96633..7313ed43b 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator_spec.rb @@ -62,8 +62,27 @@ module Binary } } ) + + @collection_comment = collection_build( + name: 'comment', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'commentable_id' => column_build(column_type: 'Number'), + 'commentable_type' => column_build, + 'commentable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'commentable_type', + foreign_collections: %w[book], + foreign_key_targets: { 'book' => 'id' }, + foreign_key: 'commentable_id' + ) + } + } + ) + datasource.add_collection(@collection_favorite) datasource.add_collection(@collection_book) + datasource.add_collection(@collection_comment) @datasource_decorator = DatasourceDecorator.new(datasource, binary_collection_decorator) @decorated_favorite = @datasource_decorator.get_collection('favorite') @@ -250,6 +269,36 @@ module Binary ] ) end + + it 'not transformed records when call list from polymorphic many to one' do + allow(@datasource_decorator.get_collection('comment')).to receive(:list) + .and_return([ + { + 'id' => 1, + 'commentable_id' => 1, + 'commentable_type' => 'book', + 'commentable' => book_record + } + ]) + + records = @datasource_decorator.get_collection('comment') + .list( + caller, + Filter.new, + Projection.new(%w[id addressable:*]) + ) + + expect(records).to eq( + [ + { + 'id' => 1, + 'commentable_id' => 1, + 'commentable_type' => 'book', + 'commentable' => book_record + } + ] + ) + end end describe 'simple creation' do diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator_spec.rb index d2647d7e0..ce1f649e8 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator_spec.rb @@ -47,6 +47,24 @@ module Computed } ) + collection_address = collection_build( + name: 'address', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'addressable_id' => ColumnSchema.new(column_type: 'Number'), + 'addressable_type' => ColumnSchema.new(column_type: 'String'), + 'street' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['person'], + foreign_key_targets: { 'person' => 'id' }, + foreign_key: 'addressable_id' + ) + } + } + ) + records = [ { 'id' => 1, @@ -69,11 +87,13 @@ module Computed datasource.add_collection(collection_book) datasource.add_collection(collection_person) + datasource.add_collection(collection_address) datasource_decorator = DatasourceDecorator.new(datasource, compute_collection_decorator) @new_books = datasource_decorator.get_collection('book') @new_persons = datasource_decorator.get_collection('person') + @new_addresses = datasource_decorator.get_collection('address') end it 'registerComputed should throw if defining a field with no dependencies' do @@ -89,6 +109,22 @@ module Computed end.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException, "🌳🌳🌳 Computed field 'newField' must have at least one dependency.") end + it 'registerComputed should throw if defining a field with polymorphic dependencies' do + expect do + @new_addresses.register_computed( + 'newField', + ComputedDefinition.new( + column_type: 'String', + dependencies: ['addressable:foo'], + values: proc { |records| records } + ) + ) + end.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 Dependencies over a polymorphic relations(address.addressable) are forbidden' + ) + end + it 'registerComputed should throw if defining a field with missing dependencies' do expect do @new_books.register_computed( diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator_spec.rb index dbf704dfa..8d2ed16b4 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator_spec.rb @@ -75,21 +75,56 @@ module Publication } ) + @collection_comment = collection_build( + name: 'comment', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'commentable_id' => column_build(column_type: 'Number'), + 'commentable_type' => column_build, + 'commentable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'commentable_type', + foreign_collections: %w[book], + foreign_key_targets: { 'book' => 'id' }, + foreign_key: 'commentable_id' + ) + } + } + ) + datasource.add_collection(@collection_book) datasource.add_collection(@collection_book_person) datasource.add_collection(@collection_person) + datasource.add_collection(@collection_comment) datasource_decorator = PublicationDatasourceDecorator.new(datasource) @decorated_book = datasource_decorator.get_collection('book') @decorated_book_person = datasource_decorator.get_collection('book_person') @decorated_person = datasource_decorator.get_collection('person') + @decorated_comment = datasource_decorator.get_collection('comment') end it 'throws when hiding a field which does not exists' do expect { @decorated_person.change_field_visibility('unknown', false) }.to raise_error(ForestException, "🌳🌳🌳 No such field 'unknown'") end + it 'raise when hiding a field referenced in a polymorphic relation' do + expect do + @decorated_comment.change_field_visibility('commentable_id', false) + end.to raise_error( + ForestException, + "🌳🌳🌳 Cannot remove field 'comment.commentable_id', because it's implied in a polymorphic relation 'comment.commentable'" + ) + + expect do + @decorated_comment.change_field_visibility('commentable_type', false) + end.to raise_error( + ForestException, + "🌳🌳🌳 Cannot remove field 'comment.commentable_type', because it's implied in a polymorphic relation 'comment.commentable'" + ) + end + it 'throws when hiding the primary key' do expect { @decorated_person.change_field_visibility('id', false) }.to raise_error(ForestException, '🌳🌳🌳 Cannot hide primary key') end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator_spec.rb index 501e4f803..d7879afc7 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator_spec.rb @@ -72,9 +72,46 @@ module Publication mark_schema_as_dirty: nil ) + @collection_user = instance_double( + ForestAdminDatasourceToolkit::Decorators::CollectionDecorator, + name: 'user', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'addresses' => Relations::PolymorphicOneToManySchema.new( + origin_key: 'addressable_id', + foreign_collection: 'address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'address' + ) + } + }, + mark_schema_as_dirty: nil + ) + + @collection_address = collection_build( + name: 'address', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'addressable_id' => column_build(column_type: 'Number'), + 'addressable_type' => column_build, + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: %w[user], + foreign_key_targets: { 'user' => 'id' }, + foreign_key: 'addressable_id' + ) + } + } + ) + datasource.add_collection(@collection_library) datasource.add_collection(@collection_library_book) datasource.add_collection(@collection_book) + datasource.add_collection(@collection_address) + datasource.add_collection(@collection_user) datasource = ForestAdminDatasourceToolkit::Decorators::DatasourceDecorator.new(datasource, Empty::EmptyCollectionDecorator) @datasource_decorator = described_class.new(datasource) @@ -96,10 +133,17 @@ module Publication expect { @datasource_decorator.keep_collections_matching(nil, ['unknown']) }.to raise_error(ForestException, '🌳🌳🌳 Collection unknown not found.') end + it 'throws an error if a collection is a target of polymorphic ManyToOne' do + expect { @datasource_decorator.keep_collections_matching(nil, ['user']) }.to raise_error( + ForestException, + "🌳🌳🌳 Cannot remove user because it's a potential target of polymorphic relation address.addressable" + ) + end + it 'is able to remove "library_book" collection' do - @datasource_decorator.keep_collections_matching(['library', 'book']) + @datasource_decorator.keep_collections_matching(%w[library book user address]) - expect { @datasource_decorator.get_collection('library_book') }.to raise_error(ForestException, "🌳🌳🌳 Collection 'library_book' was removed.") + # expect { @datasource_decorator.get_collection('library_book') }.to raise_error(ForestException, "🌳🌳🌳 Collection 'library_book' was removed.") expect(@datasource_decorator.get_collection('library').schema[:fields]).not_to have_key('my_books') expect(@datasource_decorator.get_collection('book').schema[:fields]).not_to have_key('my_libraries') end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator_spec.rb index f9147dd56..ffb959e28 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator_spec.rb @@ -233,6 +233,54 @@ module RenameCollection end end end + + context 'with Polymorphic relation' do + before do + @collection_user = collection_build( + name: 'user', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'email' => column_build, + 'address' => Relations::PolymorphicOneToOneSchema.new( + origin_key: 'addressable_id', + foreign_collection: 'address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'order' + ) + } + } + ) + + @collection_address = collection_build( + name: 'address', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'book_id' => column_build, + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['user'], + foreign_key_targets: { 'user' => 'id' }, + foreign_key: 'addressable_id' + ) + } + } + ) + + @datasource = described_class.new(datasource_with_collections_build([@collection_user, @collection_address])) + end + + describe 'rename_collection' do + it 'raise an error when collection has polymorphic relation' do + expect { @datasource.rename_collection('user', 'renamed_user') }.to raise_error( + Exceptions::ForestException, + "🌳🌳🌳 Cannot rename collection user because it's a target of a polymorphic relation 'address.addressable'" + ) + end + end + end end end end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator_spec.rb index d8981ef42..b9366a18c 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator_spec.rb @@ -89,14 +89,34 @@ module RenameField } } ) + + @collection_comment = collection_build( + name: 'comment', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'commentable_id' => column_build(column_type: 'Number'), + 'commentable_type' => column_build, + 'commentable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'commentable_type', + foreign_collections: %w[book], + foreign_key_targets: { 'book' => 'id' }, + foreign_key: 'commentable_id' + ) + } + } + ) + datasource.add_collection(@collection_person) datasource.add_collection(@collection_book_person) datasource.add_collection(@collection_book) + datasource.add_collection(@collection_comment) @datasource_decorator = DatasourceDecorator.new(datasource, rename_field_collection_decorator) @new_person = @datasource_decorator.get_collection('person') @new_book = @datasource_decorator.get_collection('book') @new_book_person = @datasource_decorator.get_collection('book_person') + @new_comment = @datasource_decorator.get_collection('comment') end it 'raise an error when renaming a field which does not exists' do @@ -105,6 +125,22 @@ module RenameField end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 No such field 'unknown'") end + it 'raise if renaming a field referenced in a polymorphic relation' do + expect do + @new_comment.rename_field('commentable_id', 'somethingnew') + end.to raise_error( + Exceptions::ForestException, + "🌳🌳🌳 Cannot rename 'comment.commentable_id', because it's implied in a polymorphic relation 'comment.commentable'" + ) + + expect do + @new_comment.rename_field('commentable_type', 'somethingnew') + end.to raise_error( + Exceptions::ForestException, + "🌳🌳🌳 Cannot rename 'comment.commentable_type', because it's implied in a polymorphic relation 'comment.commentable'" + ) + end + it 'raise an error when renaming a field using an older name' do @new_person.rename_field('id', 'key') diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator_spec.rb index 96b7eb309..8220286bd 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator_spec.rb @@ -52,7 +52,13 @@ module Search schema: { fields: { 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), - 'location' => ColumnSchema.new(column_type: 'String') + 'location' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['user'], + foreign_key_targets: { 'user' => 'id' }, + foreign_key: 'addressable_id' + ) } } ) @@ -72,6 +78,24 @@ module Search end context 'when refine_filter' do + context 'when the collection has polymorphic relation' do + it 'not search over polymorphic relations with the search extended and show a debug log' do + logger = instance_double(ForestAdminAgent::Services::LoggerService, log: nil) + allow(ForestAdminAgent::Facades::Container).to receive(:logger).and_return(logger) + filter = Filter.new(search: 'a search value', search_extended: true) + search_collection_decorator = described_class.new(datasource.get_collection('address'), datasource) + search_collection_decorator.refine_filter(caller, filter) + expect(ForestAdminAgent::Facades::Container.logger).to have_received(:log) do |level, message| + expect(level).to eq('Debug') + expect(message).to eq( + "We're not searching through address.addressable because it's a polymorphic relation. " \ + "You can override the default search behavior with 'replace_search'. " \ + 'See more: https://docs.forestadmin.com/developer-guide-agents-ruby/agent-customization/search' + ) + end + end + end + context 'when the search value is null' do it 'returns the given filter to return all records' do collection = instance_double(ForestAdminDatasourceToolkit::Collection) diff --git a/packages/forest_admin_datasource_customizer/spec/spec_helper.rb b/packages/forest_admin_datasource_customizer/spec/spec_helper.rb index 6dbe2f516..fe37cb241 100644 --- a/packages/forest_admin_datasource_customizer/spec/spec_helper.rb +++ b/packages/forest_admin_datasource_customizer/spec/spec_helper.rb @@ -6,6 +6,8 @@ require 'forest_admin_datasource_customizer' require 'shared/factory' require 'shared/column_schema_factory' +require 'filecache' +require 'singleton' SimpleCov.formatters = [SimpleCov::Formatter::JSONFormatter, SimpleCov::Formatter::HTMLFormatter] SimpleCov.start do @@ -29,6 +31,26 @@ # # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| + config.before do + cache = FileCache.new('app', 'tmp/cache/forest_admin') + cache.clear + + agent_factory = ForestAdminAgent::Builder::AgentFactory.instance + agent_factory.setup( + { + auth_secret: 'cba803d01a4d43b55010cab41fa1ea1f1f51a95e', + env_secret: '89719c6d8e2e2de2694c2f220fe2dbf02d5289487364daf1e4c6b13733ed0cdb', + is_production: false, + cache_dir: 'tmp/cache/forest_admin', + schema_path: File.join('tmp', '.forestadmin-schema.json'), + forest_server_url: 'https://api.development.forestadmin.com', + debug: true, + prefix: 'forest', + customize_error_message: nil + } + ) + end + # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. diff --git a/packages/forest_admin_datasource_toolkit/Gemfile-test b/packages/forest_admin_datasource_toolkit/Gemfile-test new file mode 100644 index 000000000..05fdf2813 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/Gemfile-test @@ -0,0 +1,14 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in forest_admin_datasource_toolkit.gemspec +gemspec + +gem 'rake', '~> 13.0' +gem 'rubocop', '~> 1.21' + +group :development, :test do + gem 'rspec', '~> 3.0' + gem 'simplecov', '~> 0.22', require: false + gem 'simplecov-html', '~> 0.12.3' + gem 'simplecov_json_formatter', '~> 0.1.4' +end diff --git a/packages/forest_admin_datasource_toolkit/Gemfile-test.lock b/packages/forest_admin_datasource_toolkit/Gemfile-test.lock new file mode 100644 index 000000000..a8ce1613c --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/Gemfile-test.lock @@ -0,0 +1,97 @@ +PATH + remote: . + specs: + forest_admin_datasource_toolkit (1.0.0.pre.beta.60) + activesupport (>= 6.1) + zeitwerk (~> 2.3) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.3.4) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.8) + concurrent-ruby (1.3.3) + connection_pool (2.4.1) + diff-lcs (1.5.1) + docile (1.4.1) + drb (2.2.1) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + json (2.7.2) + language_server-protocol (3.17.0.3) + minitest (5.24.1) + mutex_m (0.2.0) + parallel (1.25.1) + parser (3.3.4.0) + ast (~> 2.4.1) + racc + racc (1.8.0) + rainbow (3.1.1) + rake (13.2.1) + regexp_parser (2.9.2) + rexml (3.3.2) + strscan + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) + rubocop (1.65.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.4, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) + ruby-progressbar (1.13.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + strscan (3.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + zeitwerk (2.6.16) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + forest_admin_datasource_toolkit! + rake (~> 13.0) + rspec (~> 3.0) + rubocop (~> 1.21) + simplecov (~> 0.22) + simplecov-html (~> 0.12.3) + simplecov_json_formatter (~> 0.1.4) + +BUNDLED WITH + 2.4.19 diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb index 520b39374..8a0dc9af7 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb @@ -58,14 +58,24 @@ def self.make_foreign_filter(collection, id, relation_name, caller, base_foreign relation = ForestAdminDatasourceToolkit::Utils::Schema.get_to_many_relation(collection, relation_name) origin_value = ForestAdminDatasourceToolkit::Utils::Collection.get_value(collection, caller, id, relation.origin_key_target) + if relation.is_a?(OneToManySchema) origin_tree = Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value) + elsif relation.is_a?(PolymorphicOneToManySchema) + origin_tree = ConditionTreeFactory.intersect( + [ + Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value), + Nodes::ConditionTreeLeaf.new(relation.origin_type_field, Operators::EQUAL, relation.origin_type_value) + ] + ) else through_collection = collection.datasource.get_collection(relation.through_collection) - through_tree = ConditionTreeFactory.intersect([ - Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value), - Nodes::ConditionTreeLeaf.new(relation.foreign_key, Operators::PRESENT) - ]) + through_tree = ConditionTreeFactory.intersect( + [ + Nodes::ConditionTreeLeaf.new(relation.origin_key, Operators::EQUAL, origin_value), + Nodes::ConditionTreeLeaf.new(relation.foreign_key, Operators::PRESENT) + ] + ) records = through_collection.list( caller, Filter.new(condition_tree: through_tree), diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection.rb index c15a84d4d..71203b65b 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection.rb @@ -10,6 +10,8 @@ def with_pks(collection) relations.each do |relation, projection| schema = collection.schema[:fields][relation] + next unless schema.type != 'PolymorphicManyToOne' + association = collection.datasource.get_collection(schema.foreign_collection) projection_with_pks = projection.with_pks(association).nest(prefix: relation) @@ -23,15 +25,19 @@ def columns reject { |field| field.include?(':') } end - def relations - each_with_object({}) do |path, memo| + def relations(only_keys: false) + relations = each_with_object({}) do |path, memo| next unless path.include?(':') original_path = path.split(':') + next if original_path.size == 1 && !only_keys + relation = original_path.shift memo[relation] = Projection.new([original_path.join(':')].union(memo[relation] || [])) end + + only_keys ? relations.keys : relations end def nest(prefix: nil) diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb index df597b65e..93704e6c2 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/projection_factory.rb @@ -9,7 +9,7 @@ def self.all(collection) schema = path[1] memo += [column_name] if schema.type == 'Column' - if schema.type == 'OneToOne' || schema.type == 'ManyToOne' + if schema.type == 'OneToOne' || schema.type == 'ManyToOne' || schema.type == 'PolymorphicOneToOne' relation = collection.datasource.get_collection(schema.foreign_collection) relation_columns = relation.schema[:fields] .select { |_column_name, relation_column| relation_column.type == 'Column' } @@ -19,6 +19,8 @@ def self.all(collection) memo += relation_columns end + memo += ["#{column_name}:*"] if schema.type == 'PolymorphicManyToOne' + memo end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_many_to_one_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_many_to_one_schema.rb new file mode 100644 index 000000000..46610b83f --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_many_to_one_schema.rb @@ -0,0 +1,23 @@ +module ForestAdminDatasourceToolkit + module Schema + module Relations + class PolymorphicManyToOneSchema + attr_reader :foreign_key_target, :foreign_key, :foreign_key_targets, :foreign_key_type_field, + :foreign_collections, :type + + def initialize( + foreign_key_type_field:, + foreign_key:, + foreign_key_targets:, + foreign_collections: + ) + @foreign_key = foreign_key + @foreign_key_targets = foreign_key_targets + @foreign_key_type_field = foreign_key_type_field + @foreign_collections = foreign_collections + @type = 'PolymorphicManyToOne' + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_many_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_many_schema.rb new file mode 100644 index 000000000..ebe41c88c --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_many_schema.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceToolkit + module Schema + module Relations + class PolymorphicOneToManySchema < RelationSchema + attr_accessor :origin_key + attr_reader :origin_key_target, :origin_type_field, :origin_type_value + + def initialize(origin_key:, origin_key_target:, foreign_collection:, origin_type_field:, origin_type_value:) + super(foreign_collection, 'PolymorphicOneToMany') + @origin_key = origin_key + @origin_key_target = origin_key_target + @origin_type_field = origin_type_field + @origin_type_value = origin_type_value + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_one_schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_one_schema.rb new file mode 100644 index 000000000..4bd8da122 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_one_schema.rb @@ -0,0 +1,18 @@ +module ForestAdminDatasourceToolkit + module Schema + module Relations + class PolymorphicOneToOneSchema < RelationSchema + attr_accessor :origin_key + attr_reader :origin_key_target, :origin_type_field, :origin_type_value + + def initialize(origin_key:, origin_key_target:, foreign_collection:, origin_type_field:, origin_type_value:) + super(foreign_collection, 'PolymorphicOneToOne') + @origin_key = origin_key + @origin_key_target = origin_key_target + @origin_type_field = origin_type_field + @origin_type_value = origin_type_value + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb index 6773fa8f1..1f5ac3869 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb @@ -9,20 +9,25 @@ class Collection def self.get_inverse_relation(collection, relation_name) relation_field = collection.schema[:fields][relation_name] foreign_collection = collection.datasource.get_collection(relation_field.foreign_collection) + polymorphic_relations = %w[PolymorphicOneToOne PolymorphicOneToMany] inverse = foreign_collection.schema[:fields].select do |_name, field| - field.is_a?(RelationSchema) && - field.foreign_collection == collection.name && - ( - (field.is_a?(ManyToManySchema) && - relation_field.is_a?(ManyToManySchema) && - many_to_many_inverse?(field, relation_field)) || - (field.is_a?(ManyToOneSchema) && - (relation_field.is_a?(OneToOneSchema) || relation_field.is_a?(OneToManySchema)) && - many_to_one_inverse?(field, relation_field)) || - ((field.is_a?(OneToOneSchema) || field.is_a?(OneToManySchema)) && - relation_field.is_a?(ManyToOneSchema) && other_inverse?(field, relation_field)) - ) + if polymorphic_relations.include?(relation_field.type) + field.is_a?(PolymorphicManyToOneSchema) && + field.foreign_collections.include?(collection.name) + else + field.is_a?(RelationSchema) && + field.foreign_collection == collection.name && + ( + (field.is_a?(ManyToManySchema) && relation_field.is_a?(ManyToManySchema) && + many_to_many_inverse?(field, relation_field)) || + (field.is_a?(ManyToOneSchema) && + (relation_field.is_a?(OneToOneSchema) || relation_field.is_a?(OneToManySchema)) && + many_to_one_inverse?(field, relation_field)) || + ((field.is_a?(OneToOneSchema) || field.is_a?(OneToManySchema)) && + relation_field.is_a?(ManyToOneSchema) && other_inverse?(field, relation_field)) + ) + end end.keys.first inverse || nil @@ -138,7 +143,7 @@ def self.list_relation(collection, id, relation_name, caller, foreign_filter, pr projection.nest(prefix: foreign_relation) ) - return records.map { |r| r.try(foreign_relation) } + return records.map { |r| r[foreign_relation] } end end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/schema.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/schema.rb index 3b58dfedc..579cb20cb 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/schema.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/schema.rb @@ -30,7 +30,7 @@ def self.get_to_many_relation(collection, relation_name) relation = collection.schema[:fields][relation_name] - if relation.type != 'OneToMany' && relation.type != 'ManyToMany' + if relation.type != 'OneToMany' && relation.type != 'PolymorphicOneToMany' && relation.type != 'ManyToMany' raise Exceptions::ForestException, "Relation #{relation_name} has invalid type should be one of OneToMany or ManyToMany." end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/filter_factory_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/filter_factory_spec.rb index 256c50c77..759ecce3f 100644 --- a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/filter_factory_spec.rb +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/filter_factory_spec.rb @@ -242,6 +242,157 @@ module Query ) end end + + context 'when call make_foreign_filter' do + before do + @datasource = datasource_with_collections_build( + [ + collection_build( + { + name: 'Book', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'reviews' => ManyToManySchema.new( + origin_key: 'book_id', + origin_key_target: 'id', + foreign_key: 'review_id', + foreign_key_target: 'id', + foreign_collection: 'Review', + through_collection: 'BookReview' + ), + 'book_reviews' => OneToManySchema.new( + origin_key: 'book_id', + origin_key_target: 'id', + foreign_collection: 'Review' + ), + 'book_reviews_poly' => PolymorphicOneToManySchema.new( + origin_key: 'book_id', + foreign_collection: 'Review', + origin_key_target: 'id', + origin_type_field: 'reviewable_type', + origin_type_value: 'Book' + ) + } + } + } + ), + collection_build( + { + name: 'Review', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'reviewable_id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER), + 'reviewable_type' => ColumnSchema.new(column_type: PrimitiveType::STRING), + 'reviewable' => PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'reviewable_type', + foreign_collections: %w[Book], + foreign_key_targets: { 'Book' => 'id' }, + foreign_key: 'reviewable_id' + ) + } + } + } + ), + collection_build( + { + name: 'BookReview', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'book_id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER), + 'review_id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER), + 'review' => ManyToOneSchema.new( + foreign_key: 'review_id', + foreign_key_target: 'id', + foreign_collection: 'Review' + ) + } + } + } + ) + ] + ) + end + + it 'add the fk condition [one to many]' do + book = @datasource.get_collection('Book') + base_filter = Filter.new( + condition_tree: ConditionTreeLeaf.new('some_field', Operators::EQUAL, 1), + segment: 'some-segment' + ) + + filter = described_class.make_foreign_filter(book, [1], 'book_reviews', caller, base_filter) + expect(filter.condition_tree.to_h).to eq( + { + aggregator: 'And', + conditions: [ + { field: 'some_field', operator: 'equal', value: 1 }, + { field: 'book_id', operator: 'equal', value: 1 } + ] + } + ) + expect(filter.segment).to eq('some-segment') + end + + it 'query the through collection [many to many]' do + book = @datasource.get_collection('Book') + book_review = @datasource.get_collection('BookReview') + allow(book_review).to receive(:list).and_return([{ 'review_id' => 123 }, { 'review_id' => 124 }]) + + base_filter = Filter.new( + condition_tree: ConditionTreeLeaf.new('some_field', Operators::EQUAL, 1), + segment: 'some-segment' + ) + + filter = described_class.make_foreign_filter(book, [1], 'reviews', caller, base_filter) + + expect(book_review).to have_received(:list) do |_caller, through_filter| + expect(through_filter.condition_tree.to_h).to eq( + { + aggregator: 'And', + conditions: [ + { field: 'book_id', operator: 'equal', value: 1 }, + { field: 'review_id', operator: 'present', value: nil } + ] + } + ) + end + + expect(filter.condition_tree.to_h).to eq( + { + aggregator: 'And', + conditions: [ + { field: 'some_field', operator: 'equal', value: 1 }, + { field: 'id', operator: 'in', value: [123, 124] } + ] + } + ) + expect(filter.segment).to eq('some-segment') + end + + it 'add the fk condition and type [polymorphic one to many]' do + book = @datasource.get_collection('Book') + base_filter = Filter.new( + condition_tree: ConditionTreeLeaf.new('some_field', Operators::EQUAL, 1), + segment: 'some-segment' + ) + + filter = described_class.make_foreign_filter(book, [1], 'book_reviews_poly', caller, base_filter) + expect(filter.condition_tree.to_h).to eq( + { + aggregator: 'And', + conditions: [ + { field: 'some_field', operator: 'equal', value: 1 }, + { field: 'book_id', operator: 'equal', value: 1 }, + { field: 'reviewable_type', operator: 'equal', value: 'Book' } + ] + } + ) + expect(filter.segment).to eq('some-segment') + end + end end end end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/projection_factory_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/projection_factory_spec.rb new file mode 100644 index 000000000..40d7a3ed1 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/projection_factory_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Components + module Query + include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Schema::Relations + describe ProjectionFactory do + describe 'with one to one and many to one relations' do + before do + @datasource = datasource_with_collections_build( + [ + collection_build( + { + name: 'Book', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'my_author' => OneToOneSchema.new( + origin_key: 'book_id', + origin_key_target: 'id', + foreign_collection: 'Author' + ), + 'format_id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER), + 'my_format' => ManyToOneSchema.new( + foreign_key: 'format_id', + foreign_key_target: 'id', + foreign_collection: 'Format' + ), + 'title' => ColumnSchema.new(column_type: PrimitiveType::STRING) + } + } + } + ), + collection_build( + { + name: 'Author', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'book_id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER), + 'name' => ColumnSchema.new(column_type: PrimitiveType::STRING) + } + } + } + ), + collection_build( + { + name: 'Format', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'name' => ColumnSchema.new(column_type: PrimitiveType::STRING) + } + } + } + ) + ] + ) + end + + describe 'all' do + it 'return all the collection fields and the relation fields' do + collection = @datasource.get_collection('Book') + + expect(described_class.all(collection)).to eq( + %w[id my_author:id my_author:book_id my_author:name format_id my_format:id my_format:name title] + ) + end + end + end + + describe 'with other relations' do + before do + @datasource = datasource_with_collections_build( + [ + collection_build( + { + name: 'Book', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'my_book_persons' => OneToManySchema.new( + origin_key: 'book_id', + origin_key_target: 'id', + foreign_collection: 'Person' + ), + 'title' => ColumnSchema.new(column_type: PrimitiveType::STRING) + } + } + } + ), + collection_build( + { + name: 'Person', + schema: { + fields: { 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true) } + } + } + ) + ] + ) + end + + describe 'all' do + it 'return all the collection fields without the relations' do + collection = @datasource.get_collection('Book') + + expect(described_class.all(collection)).to eq( + %w[id title] + ) + end + end + end + + describe 'with polymorphic one to one and polymorphic many to one relations' do + before do + @datasource = datasource_with_collections_build( + [ + collection_build( + { + name: 'Address', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'addressable_id' => ColumnSchema.new(column_type: 'Number'), + 'addressable_type' => ColumnSchema.new(column_type: 'String'), + 'addressable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'addressable_type', + foreign_collections: ['User'], + foreign_key_targets: { 'User' => 'id' }, + foreign_key: 'addressable_id' + ) + } + } + } + ), + collection_build( + { + name: 'User', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), + 'address' => Relations::PolymorphicOneToOneSchema.new( + origin_key: 'addressable_id', + foreign_collection: 'Address', + origin_key_target: 'id', + origin_type_field: 'addressable_type', + origin_type_value: 'User' + ) + } + } + } + ) + ] + ) + end + + describe 'all' do + it 'return all the collection fields and the relation fields' do + collection = @datasource.get_collection('User') + + expect(described_class.all(collection)).to eq( + %w[id address:id address:addressable_id address:addressable_type] + ) + end + + it 'return all the collection fields and replace PolymorphicManyToOne with :*' do + collection = @datasource.get_collection('Address') + + expect(described_class.all(collection)).to eq( + %w[id addressable_id addressable_type addressable:*] + ) + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/projection_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/projection_spec.rb index 30fc105da..5333a8c13 100644 --- a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/projection_spec.rb +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/components/query/projection_spec.rb @@ -10,7 +10,13 @@ module Query collection.add_fields( { 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true), - 'name' => ColumnSchema.new(column_type: PrimitiveType::STRING) + 'name' => ColumnSchema.new(column_type: PrimitiveType::STRING), + 'polymorphic_relation' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'collection_type', + foreign_collections: %w[foo], + foreign_key_targets: { 'foo' => 'id' }, + foreign_key: 'collection_id' + ) } ) @@ -40,6 +46,23 @@ module Query projection = described_class.new(['name']).with_pks(collection) expect(projection).to eq(described_class.new(%w[name key1 key2])) end + + it 'ignore the PolymorphicManyToOne relation' do + collection = Collection.new(Datasource.new, '__collection__') + collection.add_fields( + { + 'polymorphic_relation' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'collection_type', + foreign_collections: %w[foo], + foreign_key_targets: { 'foo' => 'id' }, + foreign_key: 'collection_id' + ) + } + ) + + projection = described_class.new(%w[name polymorphic_relation:*]).with_pks(collection) + expect(projection).to eq(described_class.new(%w[name polymorphic_relation:*])) + end end describe 'nest' do @@ -70,6 +93,12 @@ module Query expect(projection.relations).to eq({ 'category' => ['label'] }) end + + it 'return only the relations keys when call with only_keys at true' do + projection = described_class.new(%w[id name category:label]) + + expect(projection.relations(only_keys: true)).to eq(['category']) + end end describe 'apply' do diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_many_to_one_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_many_to_one_schema_spec.rb new file mode 100644 index 000000000..853ad6f2f --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_many_to_one_schema_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Schema + module Relations + describe PolymorphicManyToOneSchema do + subject(:relation) do + described_class.new( + foreign_key_type_field: 'foreign_key_type_field', + foreign_collections: ['Foo'], + foreign_key_targets: { 'Foo' => 'id' }, + foreign_key: 'foreign_key' + ) + end + + describe 'getters' do + it { expect(relation.type).to eq 'PolymorphicManyToOne' } + it { expect(relation.foreign_key).to eq 'foreign_key' } + it { expect(relation.foreign_key_type_field).to eq 'foreign_key_type_field' } + it { expect(relation.foreign_key_targets).to eq({ 'Foo' => 'id' }) } + it { expect(relation.foreign_collections).to eq ['Foo'] } + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_many_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_many_schema_spec.rb new file mode 100644 index 000000000..333043f97 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_many_schema_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Schema + module Relations + describe PolymorphicOneToManySchema do + subject(:relation) do + described_class.new( + origin_key: 'origin_key', + origin_key_target: 'origin_key_target', + origin_type_field: 'origin_type_field', + origin_type_value: 'origin_type_value', + foreign_collection: 'foreign_collection' + ) + end + + describe 'getters' do + it { expect(relation.type).to eq 'PolymorphicOneToMany' } + it { expect(relation.origin_key).to eq 'origin_key' } + it { expect(relation.origin_key_target).to eq 'origin_key_target' } + it { expect(relation.origin_type_field).to eq 'origin_type_field' } + it { expect(relation.origin_type_value).to eq 'origin_type_value' } + it { expect(relation.foreign_collection).to eq 'foreign_collection' } + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_one_schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_one_schema_spec.rb new file mode 100644 index 000000000..df9e11488 --- /dev/null +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/schema/relations/polymorphic_one_to_one_schema_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +module ForestAdminDatasourceToolkit + module Schema + module Relations + describe PolymorphicOneToOneSchema do + subject(:relation) do + described_class.new( + origin_key: 'origin_key', + origin_key_target: 'origin_key_target', + origin_type_field: 'origin_type_field', + origin_type_value: 'origin_type_value', + foreign_collection: 'foreign_collection' + ) + end + + describe 'getters' do + it { expect(relation.type).to eq 'PolymorphicOneToOne' } + it { expect(relation.origin_key).to eq 'origin_key' } + it { expect(relation.origin_key_target).to eq 'origin_key_target' } + it { expect(relation.origin_type_field).to eq 'origin_type_field' } + it { expect(relation.origin_type_value).to eq 'origin_type_value' } + it { expect(relation.foreign_collection).to eq 'foreign_collection' } + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb index a39a24d37..0a872c0a4 100644 --- a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/collection_spec.rb @@ -71,6 +71,13 @@ module Utils origin_key: 'bookId', origin_key_target: 'id', foreign_collection: 'BookPerson' + ), + 'comments' => Relations::PolymorphicOneToManySchema.new( + origin_key: 'commentable_id', + foreign_collection: 'Comment', + origin_key_target: 'id', + origin_type_field: 'commentable_type', + origin_type_value: 'Book' ) } ) @@ -126,10 +133,36 @@ module Utils return collection end + let(:collection_comment) do + collection = ForestAdminDatasourceToolkit::Collection.new(datasource, 'Comment') + collection.add_fields( + { + 'id' => ColumnSchema.new(column_type: PrimitiveType::NUMBER, is_primary_key: true, + filter_operators: [ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators::IN, ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators::EQUAL]), + 'name' => ColumnSchema.new(column_type: PrimitiveType::STRING), + 'commentable_id' => ColumnSchema.new(column_type: 'Number'), + 'commentable_type' => ColumnSchema.new(column_type: 'String'), + 'commentable' => Relations::PolymorphicManyToOneSchema.new( + foreign_key_type_field: 'commentable_type', + foreign_collections: %w[Book], + foreign_key_targets: { 'Book' => 'id' }, + foreign_key: 'commentable_id' + ) + } + ) + + return collection + end + before do datasource.add_collection(collection_book) datasource.add_collection(collection_book_person) datasource.add_collection(collection_person) + datasource.add_collection(collection_comment) + end + + it 'get_inverse_relation should inverse a polymorphic one to many relation' do + expect(described_class.get_inverse_relation(collection_book, 'comments')).to eq('commentable') end it 'get_inverse_relation should inverse a one to many relation in both directions' do diff --git a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/schema_spec.rb b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/schema_spec.rb index fdc59d4d3..5b25f4b52 100644 --- a/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/schema_spec.rb +++ b/packages/forest_admin_datasource_toolkit/spec/lib/forest_admin_datasource_toolkit/utils/schema_spec.rb @@ -15,6 +15,26 @@ module Utils foreign_key: 'author_id', foreign_key_target: 'id', foreign_collection: 'Person' + ), + 'myBooks' => Relations::ManyToManySchema.new( + origin_key: 'personId', + origin_key_target: 'id', + foreign_key: 'bookId', + foreign_key_target: 'id', + foreign_collection: 'Book', + through_collection: 'BookPerson' + ), + 'myBookPersons' => Relations::OneToManySchema.new( + origin_key: 'bookId', + origin_key_target: 'id', + foreign_collection: 'BookPerson' + ), + 'comments' => Relations::PolymorphicOneToManySchema.new( + origin_key: 'commentable_id', + foreign_collection: 'Comment', + origin_key_target: 'id', + origin_type_field: 'commentable_type', + origin_type_value: 'Book' ) } ) @@ -47,6 +67,30 @@ module Utils expect(described_class.primary_keys(collection)).to eq(%w[id composite_id]) end end + + describe 'get_to_many_relation' do + it 'raise an error when relation do not exist' do + expect { described_class.get_to_many_relation(collection, 'foo') }.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, '🌳🌳🌳 Relation foo not found' + ) + end + + it 'raise an error when the relation is not a to_many relation' do + expect { described_class.get_to_many_relation(collection, 'author') }.to raise_error( + ForestAdminDatasourceToolkit::Exceptions::ForestException, + '🌳🌳🌳 Relation author has invalid type should be one of OneToMany or ManyToMany.' + ) + end + + it 'return the relation' do + expect(described_class.get_to_many_relation(collection, 'comments')).to eq( + collection.schema[:fields]['comments'] + ) + expect(described_class.get_to_many_relation(collection, 'myBookPersons')).to eq( + collection.schema[:fields]['myBookPersons'] + ) + end + end end end end diff --git a/packages/forest_admin_datasource_toolkit/spec/shared/factory.rb b/packages/forest_admin_datasource_toolkit/spec/shared/factory.rb index 3e5b357fe..e239b7346 100644 --- a/packages/forest_admin_datasource_toolkit/spec/shared/factory.rb +++ b/packages/forest_admin_datasource_toolkit/spec/shared/factory.rb @@ -30,7 +30,7 @@ def collection_build(args = {}) update: nil, delete: nil, aggregate: nil, - **args.except!(:schema) + **args.except(:schema) } ) end diff --git a/packages/forest_admin_rails/Gemfile-test b/packages/forest_admin_rails/Gemfile-test new file mode 100644 index 000000000..aa75d4834 --- /dev/null +++ b/packages/forest_admin_rails/Gemfile-test @@ -0,0 +1,12 @@ +source "https://rubygems.org" +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +gemspec + +group :development, :test do + gem 'forest_admin_agent', path: '../forest_admin_agent' + gem 'forest_admin_datasource_active_record', path: '../forest_admin_datasource_active_record' + gem 'rspec-rails', '~> 6.0.0' + gem 'simplecov', "~> 0.22", require: false + gem 'simplecov_json_formatter', "~> 0.1.4" +end