diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb index c25bf2b39..a350ae13b 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/collection_customizer.rb @@ -191,6 +191,15 @@ def remove_field(*names) end end + # Rename fields from the exported schema. + # @param current_name the current name of the field or the relation in a given collection + # @param new_name the new name of the field or the relation + # @example + # rename_field('currentFieldOrRelationName', 'newFieldOrRelationName') + def rename_field(current_name, new_name) + push_customization { @stack.rename_field.get_collection(@name).rename_field(current_name, new_name) } + end + private def push_customization(&customization) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/datasource_customizer.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/datasource_customizer.rb index 7a04b6aff..32bfdc4e1 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/datasource_customizer.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/datasource_customizer.rb @@ -35,7 +35,13 @@ def add_datasource(datasource, options) datasource = publication_decorator end - # TODO: add rename behavior + if options[:rename] + rename_collection_decorator = Decorators::RenameCollection::RenameCollectionDatasourceDecorator.new( + datasource + ) + rename_collection_decorator.rename_collections(options[:rename]) + datasource = rename_collection_decorator + end datasource.collections.each_value do |collection| @composite_datasource.add_collection(collection) 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 new file mode 100644 index 000000000..f1fc5dad0 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb @@ -0,0 +1,70 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module RenameCollection + class RenameCollectionDatasourceDecorator < ForestAdminDatasourceToolkit::Decorators::DatasourceDecorator + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Decorators + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + + def initialize(child_datasource) + @from_child_name = {} + @to_child_name = {} + super(child_datasource, RenameCollectionDecorator) + end + + def collections + @child_datasource.collections.to_h do |name, _collection| + [name, method(:get_collection).super_method.call(name)] + end + end + + def get_collection(name) + # Collection has been renamed, user is using the new name + return super(@to_child_name[name]) if @to_child_name.key?(name) + + # Collection has been renamed, user is using the old name + if @from_child_name.key?(name) + raise Exceptions::ForestException, "Collection '#{name}' has been renamed to '#{@from_child_name[name]}'" + end + + # Collection has not been renamed + super(name) + end + + def rename_collections(renames = []) + renames.each do |current_name, new_name| + rename_collection(current_name, new_name) + end + end + + def rename_collection(current_name, new_name) + # Check collection exists + get_collection(current_name) + + return unless current_name != new_name + + # Check new name is not already used + if collections.any? { |name, _collection| name == new_name } + raise Exceptions::ForestException, + "The given new collection name '#{new_name}' is already defined" + end + + # Check we don't rename a collection twice + if @to_child_name[current_name] + raise Exceptions::ForestException, + "Cannot rename a collection twice: #{@to_child_name[current_name]}->#{current_name}->#{new_name}" + end + + @from_child_name[current_name] = new_name + @to_child_name[new_name] = current_name + + collections.each_value(&:mark_schema_as_dirty) + end + + def get_collection_name(child_name) + @from_child_name[child_name] || child_name + end + end + end + end +end 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 new file mode 100644 index 000000000..2ebca46f5 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb @@ -0,0 +1,37 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module RenameCollection + class RenameCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator + include ForestAdminDatasourceToolkit::Decorators + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + + def name + datasource.get_collection_name(super) + end + + def refine_schema(sub_schema) + fields = {} + + sub_schema[:fields].each do |name, old_schema| + if old_schema.type != 'Column' + 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) + end + end + + fields[name] = old_schema + end + + sub_schema + end + + # rubocop:disable Lint/UselessMethodDefinition + def mark_schema_as_dirty + super + end + # rubocop:enable Lint/UselessMethodDefinition + end + end + end +end 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 5cf573185..993874280 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 @@ -33,20 +33,20 @@ def rename_field(current_name, new_name) def refine_schema(sub_schema) fields = {} - sub_schema[:fields].each do |old_name, schema| - case schema.type + sub_schema[:fields].each do |old_name, old_schema| + case old_schema.type when 'ManyToOne' - schema.foreign_key = from_child_collection[schema.foreign_key] || schema.foreign_key + old_schema.foreign_key = from_child_collection[old_schema.foreign_key] || old_schema.foreign_key when 'OneToMany', 'OneToOne' - relation = datasource.get_collection(schema.foreign_collection) - schema.origin_key = relation.from_child_collection[schema.origin_key] || schema.origin_key + relation = datasource.get_collection(old_schema.foreign_collection) + old_schema.origin_key = relation.from_child_collection[old_schema.origin_key] || old_schema.origin_key when 'ManyToMany' - through = datasource.get_collection(schema.through_collection) - schema.foreign_key = through.from_child_collection[schema.foreign_key] || schema.foreign_key - schema.origin_key = through.from_child_collection[schema.origin_key] || schema.origin_key + through = datasource.get_collection(old_schema.through_collection) + old_schema.foreign_key = through.from_child_collection[old_schema.foreign_key] || old_schema.foreign_key + old_schema.origin_key = through.from_child_collection[old_schema.origin_key] || old_schema.origin_key end - fields[from_child_collection[old_name] || old_name] = schema + fields[from_child_collection[old_name] || old_name] = old_schema end sub_schema[:fields] = fields @@ -69,7 +69,7 @@ def refine_filter(_caller, filter = nil) end def create(caller, data) - record = super( + record = @child_collection.create( caller, record_to_child_collection(data) ) @@ -79,18 +79,18 @@ def create(caller, data) def list(caller, filter, projection) child_projection = projection.replace { |field| path_to_child_collection(field) } - records = super(caller, filter, child_projection) + records = @child_collection.list(caller, filter, child_projection) return records if child_projection == projection records.map { |record| record_from_child_collection(record) } end def update(caller, filter, patch) - super(caller, filter, record_to_child_collection(patch)) + @child_collection.update(caller, filter, record_to_child_collection(patch)) end def aggregate(caller, filter, aggregation, limit = nil) - rows = super( + rows = @child_collection.aggregate( caller, filter, aggregation.replace_fields { |field| path_to_child_collection(field) }, diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb index 4554da0d8..17ba99fd7 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/collection_customizer_spec.rb @@ -417,5 +417,17 @@ module ForestAdminDatasourceCustomizer expect(publication_collection.fields).not_to have_key('name_in_read_only') end end + + context 'when using rename_field' do + it 'replace rename a field' do + customizer = described_class.new(@datasource_customizer, @datasource_customizer.stack, 'person') + customizer.rename_field('name', 'renamed_name') + @datasource_customizer.datasource({}) + + rename_collection = @datasource_customizer.stack.rename_field.get_collection('person') + + expect(rename_collection.schema[:fields]).to have_key('renamed_name') + end + end end end 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 dfa69d067..016a30a0f 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 @@ -25,9 +25,25 @@ module ForestAdminDatasourceCustomizer customized = described_class.new .add_datasource(datasource, {}) .remove_collection('collection') + customized.stack.apply_queued_customizations({}) expect(customized.collections).to be_empty end end + + context 'when rename a collection' do + it 'rename the collection into the datasource' do + customized = described_class.new + .add_datasource( + datasource, + { + rename: { 'collection' => 'renamed_collection' } + } + ) + customized.stack.apply_queued_customizations({}) + + expect(customized.collections.key?('renamed_collection')).to be(true) + end + end end 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 new file mode 100644 index 000000000..f9147dd56 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' +require 'shared/caller' + +module ForestAdminDatasourceCustomizer + module Decorators + module RenameCollection + include ForestAdminDatasourceToolkit + include ForestAdminDatasourceToolkit::Components::Query + include ForestAdminDatasourceToolkit::Components::Query::ConditionTree + include ForestAdminDatasourceToolkit::Decorators + include ForestAdminDatasourceToolkit::Schema + + describe RenameCollectionDatasourceDecorator do + include_context 'with caller' + subject(:rename_collection_decorator) { described_class } + + it 'return the real name when it is not renamed' do + datasource = datasource_with_collections_build( + [collection_build(name: 'foo', schema: { fields: { 'id' => numeric_primary_key_build } })] + ) + decorated_datasource = described_class.new(datasource) + collection = decorated_datasource.get_collection('foo') + + expect(collection.name).to eq('foo') + end + + it 'return the new name when it is renamed' do + datasource = datasource_with_collections_build( + [collection_build(name: 'foo', schema: { fields: { 'id' => numeric_primary_key_build } })] + ) + decorated_datasource = described_class.new(datasource) + decorated_datasource.rename_collection('foo', 'new_name') + + expect(decorated_datasource.get_collection('new_name')).not_to be_nil + expect do + decorated_datasource.get_collection('foo') + end.to raise_error(Exceptions::ForestException) + end + + context 'with ManyToMany relation' do + before do + @collection_library = collection_build( + name: 'library', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'many_to_many_relation' => many_to_many_build( + foreign_collection: 'book', + origin_key: 'library_id', + through_collection: 'library_book', + foreign_key: 'book_id' + ) + } + } + ) + + @collection_library_book = collection_build( + name: 'library_book', + schema: { + fields: { + 'book_id' => numeric_primary_key_build, + 'library_id' => numeric_primary_key_build, + 'my_book' => many_to_one_build(foreign_collection: 'book', foreign_key: 'book_id'), + 'my_library' => many_to_one_build(foreign_collection: 'library', foreign_key: 'library_id') + } + } + ) + + @collection_book = collection_build( + name: 'book', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'many_to_many_relation' => many_to_many_build( + foreign_collection: 'library', + origin_key: 'book_id', + through_collection: 'library_book', + foreign_key: 'library_id' + ) + } + } + ) + + @datasource = described_class.new( + datasource_with_collections_build( + [ + @collection_book, + @collection_library_book, + @collection_library + ] + ) + ) + end + + describe 'rename_collection' do + it 'raise an error if the given new name is already used' do + expect do + @datasource.rename_collection('library_book', 'book') + end.to raise_error( + Exceptions::ForestException, + "🌳🌳🌳 The given new collection name 'book' is already defined" + ) + end + + it 'raise an error if renaming twice' do + @datasource.rename_collection('book', 'book2') + expect do + @datasource.rename_collection('book2', 'book3') + end.to raise_error( + Exceptions::ForestException, + '🌳🌳🌳 Cannot rename a collection twice: book->book2->book3' + ) + end + + it 'raise an error if the given old name does not exist' do + expect do + @datasource.rename_collection('doesNotExist', 'book') + end.to raise_error( + Exceptions::ForestException, + '🌳🌳🌳 Collection doesNotExist not found.' + ) + end + + it 'change the foreign collection when it is a many to many' do + @datasource.rename_collection('library_book', 'renamed_library_book') + @datasource.rename_collection('book', 'renamed_book') + collection = @datasource.get_collection('library') + + expect(collection.schema[:fields]['many_to_many_relation']).to have_attributes( + foreign_collection: 'renamed_book', + origin_key: 'library_id', + through_collection: 'renamed_library_book', + foreign_key: 'book_id' + ) + end + end + + describe 'rename_collections' do + it 'work with undefined' do + @datasource.rename_collections + + expect(@datasource.collections.keys).to include('library_book') + end + + it 'work when using a hash' do + @datasource.rename_collections({ 'library_book' => 'renamed_library_book' }) + collection_name = @datasource.collections.map { |_key, collection| collection.name } + + expect(collection_name).to include('renamed_library_book') + end + end + end + + context 'with OneToOne relation' do + before do + @collection_book = collection_build( + name: 'book', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'owner' => one_to_one_build(foreign_collection: 'owner', origin_key: 'book_id') + } + } + ) + + @collection_owner = collection_build( + name: 'owner', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'book_id' => column_build, + 'owner' => many_to_one_build(foreign_collection: 'owner', foreign_key: 'book_id') + } + } + ) + + @datasource = described_class.new(datasource_with_collections_build([@collection_book, @collection_owner])) + end + + describe 'rename_collection' do + it 'change the foreign collection when it is a one to one' do + @datasource.rename_collection('owner', 'renamed_owner') + collection = @datasource.get_collection('book') + + expect(collection.schema[:fields]['owner']).to have_attributes( + foreign_collection: 'renamed_owner', + origin_key: 'book_id', + origin_key_target: 'id', + type: 'OneToOne' + ) + end + end + end + + context 'with ManyToOne and OneToMany relation' do + before do + @collection_book = collection_build( + name: 'book', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'person_id' => column_build, + 'my_person' => many_to_one_build(foreign_collection: 'person', foreign_key: 'person_id') + } + } + ) + + @collection_person = collection_build( + name: 'person', + schema: { + fields: { + 'id' => numeric_primary_key_build, + 'name' => column_build, + 'my_books' => one_to_many_build(foreign_collection: 'book', origin_key: 'id') + } + } + ) + + @datasource = described_class.new(datasource_with_collections_build([@collection_book, @collection_person])) + end + + describe 'rename_collection' do + it 'change the foreign collection when it is a many to one' do + @datasource.rename_collection('person', 'renamed_person') + collection = @datasource.get_collection('book') + + expect(collection.schema[:fields]['my_person']).to have_attributes( + foreign_collection: 'renamed_person', + foreign_key: 'person_id', + foreign_key_target: 'id', + type: 'ManyToOne' + ) + end + end + end + end + end + end +end diff --git a/packages/forest_admin_datasource_customizer/spec/shared/column_schema_factory.rb b/packages/forest_admin_datasource_customizer/spec/shared/column_schema_factory.rb new file mode 100644 index 000000000..4de08d8f4 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/shared/column_schema_factory.rb @@ -0,0 +1,42 @@ +module ColumnSchemaFactory + include ForestAdminDatasourceToolkit::Schema + include ForestAdminDatasourceToolkit::Validations + + def column_build(args = {}) + ColumnSchema.new(column_type: 'String', **args) + end + + def numeric_primary_key_build(args = {}) + ColumnSchema.new( + is_primary_key: true, + column_type: 'Number', + filter_operators: Rules.get_allowed_operators_for_column_type('Number'), + **args + ) + end + + def uuid_primary_key_build(args = {}) + ColumnSchema.new( + is_primary_key: true, + column_type: 'Uuid', + filter_operators: Rules.get_allowed_operators_for_column_type('Uuid'), + **args + ) + end + + def many_to_many_build(args = {}) + Relations::ManyToManySchema.new(origin_key_target: 'id', foreign_key_target: 'id', **args) + end + + def many_to_one_build(args = {}) + Relations::ManyToOneSchema.new(foreign_key_target: 'id', **args) + end + + def one_to_one_build(args = {}) + Relations::OneToOneSchema.new(origin_key_target: 'id', **args) + end + + def one_to_many_build(args = {}) + Relations::OneToManySchema.new(origin_key_target: 'id', **args) + end +end diff --git a/packages/forest_admin_datasource_customizer/spec/shared/factory.rb b/packages/forest_admin_datasource_customizer/spec/shared/factory.rb new file mode 100644 index 000000000..007108bdc --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/shared/factory.rb @@ -0,0 +1,35 @@ +module Factory + def datasource_with_collections_build(collections) + datasource = ForestAdminDatasourceToolkit::Datasource.new + collections.each do |collection| + allow(collection).to receive(:datasource).and_return(datasource) + datasource.add_collection(collection) + end + + datasource + end + + def collection_build(args = {}) + instance_double( + ForestAdminDatasourceToolkit::Collection, + { + datasource: ForestAdminDatasourceToolkit::Datasource.new, + name: 'collection', + schema: { + fields: {}, + countable: false, + searchable: false + }, + execute: nil, + get_form: nil, + render_chart: nil, + create: nil, + list: nil, + update: nil, + delete: nil, + aggregate: nil, + **args + } + ) + end +end diff --git a/packages/forest_admin_datasource_customizer/spec/spec_helper.rb b/packages/forest_admin_datasource_customizer/spec/spec_helper.rb index eedee31a3..f826ee016 100644 --- a/packages/forest_admin_datasource_customizer/spec/spec_helper.rb +++ b/packages/forest_admin_datasource_customizer/spec/spec_helper.rb @@ -3,6 +3,8 @@ require 'simplecov-html' require 'forest_admin_datasource_toolkit' require 'forest_admin_datasource_customizer' +require 'shared/factory' +require 'shared/column_schema_factory' SimpleCov.formatters = [SimpleCov::Formatter::JSONFormatter, SimpleCov::Formatter::HTMLFormatter] SimpleCov.start do @@ -55,4 +57,7 @@ # inherited by the metadata hash of host groups and examples, rather than # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups + + config.include Factory + config.include ColumnSchemaFactory end