diff --git a/.rubocop.yml b/.rubocop.yml index 41ea6784f..b81c99d03 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -329,6 +329,7 @@ Metrics/ClassLength: - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.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/publication/publication_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_collection/rename_collection_decorator.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_collection.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_collection.rb index e8e4ec10f..8f169020f 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_collection.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_collection.rb @@ -26,7 +26,7 @@ def self.build_fields(collection) ForestAdminDatasourceToolkit::Utils::Schema.primary_key?(collection, name) end - fields.map { |name, _field| GeneratorField.build_schema(collection, name) } + fields.filter_map { |name, _field| GeneratorField.build_schema(collection, name) } .sort_by { |v| v[:field] } end 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 3f5a10565..835494aa4 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 @@ -21,6 +21,8 @@ def self.build_schema(collection, name) build_relation_schema(collection, name) end + return nil if schema.nil? + schema.sort_by { |k, _v| k }.to_h end @@ -81,6 +83,22 @@ def build_many_to_many_schema(relation, collection, foreign_collection, base_sch foreign_schema = through_schema.schema[:fields][relation.foreign_key] origin_key = through_schema.schema[:fields][relation.origin_key] + unless target_field + log_missing_relation_field(foreign_collection.name, target_name, 'foreign_key_target', + base_schema[:field]) + return nil + end + + unless foreign_schema + log_missing_relation_field(through_schema.name, relation.foreign_key, 'foreign_key', base_schema[:field]) + return nil + end + + unless origin_key + log_missing_relation_field(through_schema.name, relation.origin_key, 'origin_key', base_schema[:field]) + return nil + end + base_schema.merge( { type: [target_field.column_type], @@ -101,6 +119,17 @@ def build_one_to_many_schema(relation, collection, foreign_collection, base_sche target_field = collection.schema[:fields][target_name] origin_key = foreign_collection.schema[:fields][relation.origin_key] + unless target_field + log_missing_relation_field(collection.name, target_name, 'origin_key_target', base_schema[:field]) + return nil + end + + unless origin_key + log_missing_relation_field(foreign_collection.name, relation.origin_key, 'origin_key', + base_schema[:field]) + return nil + end + base_schema.merge( { type: [target_field.column_type], @@ -120,6 +149,18 @@ def build_one_to_one_schema(relation, collection, foreign_collection, base_schem target_field = collection.schema[:fields][relation.origin_key_target] key_field = foreign_collection.schema[:fields][relation.origin_key] + unless target_field + log_missing_relation_field(collection.name, relation.origin_key_target, 'origin_key_target', + base_schema[:field]) + return nil + end + + unless key_field + log_missing_relation_field(foreign_collection.name, relation.origin_key, 'origin_key', + base_schema[:field]) + return nil + end + base_schema.merge( { type: key_field.column_type, @@ -138,6 +179,11 @@ def build_one_to_one_schema(relation, collection, foreign_collection, base_schem def build_many_to_one_schema(relation, collection, foreign_collection, base_schema) key_field = collection.schema[:fields][relation.foreign_key] + unless key_field + log_missing_relation_field(collection.name, relation.foreign_key, 'foreign_key', base_schema[:field]) + return nil + end + base_schema.merge( { type: key_field.column_type, @@ -202,6 +248,13 @@ def build_relation_schema(collection, name) end end end + + def log_missing_relation_field(collection_name, field_name, field_type, relation_name) + message = "Field '#{field_name}' (#{field_type}) not found in collection '#{collection_name}' " \ + "for relation '#{relation_name}'. This relation will be skipped. " \ + 'Check if the field exists in your database.' + Facades::Container.logger.log('Warn', message) + end end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_collection_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_collection_spec.rb index 54291a110..b3530a9cb 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_collection_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_collection_spec.rb @@ -70,6 +70,49 @@ module Schema relationship: 'HasOne' ) end + + context 'when relations have missing fields' do + before do + @datasource_with_bad_relations = Datasource.new + + collection_order = Collection.new(@datasource_with_bad_relations, 'Order') + collection_order.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'total' => ColumnSchema.new(column_type: 'Number'), + # Relation with missing foreign_key + 'customer' => Relations::ManyToOneSchema.new( + foreign_key: 'missing_customer_id', + foreign_key_target: 'id', + foreign_collection: 'Customer' + ) + } + ) + + collection_customer = Collection.new(@datasource_with_bad_relations, 'Customer') + collection_customer.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'name' => ColumnSchema.new(column_type: 'String') + } + ) + + @datasource_with_bad_relations.add_collection(collection_order) + @datasource_with_bad_relations.add_collection(collection_customer) + end + + it 'filters out relations with missing fields from schema' do + logger = instance_spy(Logger) + allow(Facades::Container).to receive(:logger).and_return(logger) + + schema = described_class.build_schema(@datasource_with_bad_relations.get_collection('Order')) + + # Should only have 'id' and 'total', not 'customer' (which has missing foreign_key) + field_names = schema[:fields].map { |f| f[:field] } + expect(field_names).to contain_exactly('id', 'total') + expect(field_names).not_to include('customer') + end + end end end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_spec.rb index 49b17c5cd..66ae04dbf 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_field_spec.rb @@ -111,6 +111,100 @@ module Schema ] ) end + + context 'when relation has missing fields' do + before do + @datasource_with_bad_relations = Datasource.new + + collection_order = Collection.new(@datasource_with_bad_relations, 'Order') + collection_order.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + # user_id field is missing but relation references it + 'user' => Relations::ManyToOneSchema.new( + foreign_key: 'missing_user_id', + foreign_key_target: 'id', + foreign_collection: 'User' + ) + } + ) + + collection_user = Collection.new(@datasource_with_bad_relations, 'User') + collection_user.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'name' => ColumnSchema.new(column_type: 'String') + } + ) + + @datasource_with_bad_relations.add_collection(collection_order) + @datasource_with_bad_relations.add_collection(collection_user) + end + + it 'returns nil and logs a warning when foreign_key field is missing in ManyToOne relation' do + logger = instance_spy(Logger) + allow(Facades::Container).to receive(:logger).and_return(logger) + + schema = described_class.build_schema( + @datasource_with_bad_relations.get_collection('Order'), + 'user' + ) + + expect(schema).to be_nil + expect(logger).to have_received(:log).with( + 'Warn', + "Field 'missing_user_id' (foreign_key) not found in collection 'Order' for relation 'user'. " \ + 'This relation will be skipped. Check if the field exists in your database.' + ) + end + end + + context 'when OneToMany relation has missing origin_key' do + before do + @datasource_with_bad_one_to_many = Datasource.new + + collection_author = Collection.new(@datasource_with_bad_one_to_many, 'Author') + collection_author.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'books' => Relations::OneToManySchema.new( + origin_key: 'missing_author_id', + origin_key_target: 'id', + foreign_collection: 'Book' + ) + } + ) + + collection_book = Collection.new(@datasource_with_bad_one_to_many, 'Book') + collection_book.add_fields( + { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'title' => ColumnSchema.new(column_type: 'String') + # author_id is missing + } + ) + + @datasource_with_bad_one_to_many.add_collection(collection_author) + @datasource_with_bad_one_to_many.add_collection(collection_book) + end + + it 'returns nil and logs a warning when origin_key field is missing' do + logger = instance_spy(Logger) + allow(Facades::Container).to receive(:logger).and_return(logger) + + schema = described_class.build_schema( + @datasource_with_bad_one_to_many.get_collection('Author'), + 'books' + ) + + expect(schema).to be_nil + expect(logger).to have_received(:log).with( + 'Warn', + "Field 'missing_author_id' (origin_key) not found in collection 'Book' for relation 'books'. " \ + 'This relation will be skipped. Check if the field exists in your database.' + ) + end + end end end end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer.rb index d865f6128..e3135284c 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer.rb @@ -1,4 +1,5 @@ require_relative 'forest_admin_datasource_customizer/version' +require 'set' require 'zeitwerk' loader = Zeitwerk::Loader.for_gem 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 1ad8cf884..ce0533252 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 @@ -55,7 +55,15 @@ def refine_schema(child_schema) schema end - def published?(name) + def published?(name, visited = nil) + # Track visited collection:field pairs to prevent infinite recursion + # when checking bidirectional relations (e.g., users -> orders -> users) + visited ||= Set.new + visit_key = "#{self.name}:#{name}" + return true if visited.include?(visit_key) + + visited.add(visit_key) + # Explicitly hidden return false if @blacklist.include?(name) @@ -68,32 +76,13 @@ def published?(name) return false end - if field.type == 'ManyToOne' - return ( - datasource.published?(field.foreign_collection) && - published?(field.foreign_key) && - datasource.get_collection(field.foreign_collection).published?(field.foreign_key_target) - ) - end + return check_many_to_one_published?(field, visited) if field.type == 'ManyToOne' if %w[OneToOne OneToMany PolymorphicOneToOne PolymorphicOneToMany].include?(field.type) - return ( - datasource.published?(field.foreign_collection) && - datasource.get_collection(field.foreign_collection).published?(field.origin_key) && - published?(field.origin_key_target) - ) + return check_one_to_x_published?(field, visited) end - if field.type == 'ManyToMany' - return ( - datasource.published?(field.through_collection) && - datasource.published?(field.foreign_collection) && - datasource.get_collection(field.through_collection).published?(field.foreign_key) && - datasource.get_collection(field.through_collection).published?(field.origin_key) && - published?(field.origin_key_target) && - datasource.get_collection(field.foreign_collection).published?(field.foreign_key_target) - ) - end + return check_many_to_many_published?(field, visited) if field.type == 'ManyToMany' true end @@ -103,6 +92,79 @@ def mark_schema_as_dirty super end # rubocop:enable Lint/UselessMethodDefinition + + private + + def check_many_to_one_published?(field, visited) + return false unless datasource.published?(field.foreign_collection) + return false unless published?(field.foreign_key, visited) + + foreign_collection = datasource.get_collection(field.foreign_collection) + unless foreign_collection.child_collection.schema[:fields].key?(field.foreign_key_target) + log_missing_field(field.foreign_collection, field.foreign_key_target, 'foreign_key_target') + return false + end + + foreign_collection.published?(field.foreign_key_target, visited) + end + + def check_one_to_x_published?(field, visited) + return false unless datasource.published?(field.foreign_collection) + + foreign_collection = datasource.get_collection(field.foreign_collection) + unless foreign_collection.child_collection.schema[:fields].key?(field.origin_key) + log_missing_field(field.foreign_collection, field.origin_key, 'origin_key') + return false + end + + return false unless foreign_collection.published?(field.origin_key, visited) + + unless child_collection.schema[:fields].key?(field.origin_key_target) + log_missing_field(name, field.origin_key_target, 'origin_key_target') + return false + end + + published?(field.origin_key_target, visited) + end + + def check_many_to_many_published?(field, visited) + return false unless datasource.published?(field.through_collection) + return false unless datasource.published?(field.foreign_collection) + + through_collection = datasource.get_collection(field.through_collection) + foreign_collection = datasource.get_collection(field.foreign_collection) + + unless through_collection.child_collection.schema[:fields].key?(field.foreign_key) + log_missing_field(field.through_collection, field.foreign_key, 'foreign_key') + return false + end + + unless through_collection.child_collection.schema[:fields].key?(field.origin_key) + log_missing_field(field.through_collection, field.origin_key, 'origin_key') + return false + end + + unless child_collection.schema[:fields].key?(field.origin_key_target) + log_missing_field(name, field.origin_key_target, 'origin_key_target') + return false + end + + unless foreign_collection.child_collection.schema[:fields].key?(field.foreign_key_target) + log_missing_field(field.foreign_collection, field.foreign_key_target, 'foreign_key_target') + return false + end + + through_collection.published?(field.foreign_key, visited) && + through_collection.published?(field.origin_key, visited) && + published?(field.origin_key_target, visited) && + foreign_collection.published?(field.foreign_key_target, visited) + end + + def log_missing_field(collection_name, field_name, field_type) + message = "Field '#{field_name}' (#{field_type}) not found in schema of collection '#{collection_name}'. " \ + 'This relation will be hidden. Check if the field exists in your database.' + ForestAdminAgent::Facades::Container.logger.log('Warn', message) + end end end end 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 d9436d1ed..4800d7af1 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 @@ -105,7 +105,7 @@ module Publication 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'") + 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 @@ -113,19 +113,19 @@ module Publication @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'" + /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'" + /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') + expect { @decorated_person.change_field_visibility('id', false) }.to raise_error(ForestException, /Cannot hide primary key/) end it 'the schema should be the same when doing nothing' do @@ -203,6 +203,107 @@ module Publication expect(result).to be(false) end end + + context 'when checking bidirectional relations (circular references)' do + before do + @collection_user = instance_double( + ForestAdminDatasourceToolkit::Collection, + name: 'user', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'orders' => Relations::OneToManySchema.new( + foreign_collection: 'order', + origin_key: 'user_id', + origin_key_target: 'id' + ) + } + } + ) + + @collection_order = instance_double( + ForestAdminDatasourceToolkit::Collection, + name: 'order', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'user_id' => ColumnSchema.new(column_type: 'Number'), + 'user' => Relations::ManyToOneSchema.new( + foreign_collection: 'user', + foreign_key: 'user_id', + foreign_key_target: 'id' + ) + } + } + ) + + @circular_datasource = ForestAdminDatasourceToolkit::Datasource.new + @circular_datasource.add_collection(@collection_user) + @circular_datasource.add_collection(@collection_order) + + @circular_datasource_decorator = PublicationDatasourceDecorator.new(@circular_datasource) + @decorated_user = @circular_datasource_decorator.get_collection('user') + @decorated_order = @circular_datasource_decorator.get_collection('order') + end + + it 'does not cause infinite recursion when checking published on bidirectional relations' do + # This should not raise SystemStackError (stack level too deep) + expect { @decorated_user.published?('orders') }.not_to raise_error + expect { @decorated_order.published?('user') }.not_to raise_error + end + + it 'returns true for valid bidirectional relations' do + expect(@decorated_user.published?('orders')).to be(true) + expect(@decorated_order.published?('user')).to be(true) + end + + it 'schema includes bidirectional relations without infinite recursion' do + # This should not raise SystemStackError + expect { @decorated_user.schema }.not_to raise_error + expect { @decorated_order.schema }.not_to raise_error + + expect(@decorated_user.schema[:fields]).to have_key('orders') + expect(@decorated_order.schema[:fields]).to have_key('user') + end + end + + context 'when relation has missing foreign key target' do + before do + @collection_with_bad_relation = instance_double( + ForestAdminDatasourceToolkit::Collection, + name: 'bad_collection', + schema: { + fields: { + 'id' => ColumnSchema.new(column_type: 'Number', is_primary_key: true), + 'related' => Relations::ManyToOneSchema.new( + foreign_collection: 'person', + foreign_key: 'related_id', + foreign_key_target: 'missing_field' + ), + 'related_id' => ColumnSchema.new(column_type: 'Number') + } + } + ) + + datasource.add_collection(@collection_with_bad_relation) + datasource_decorator = PublicationDatasourceDecorator.new(datasource) + @decorated_bad = datasource_decorator.get_collection('bad_collection') + end + + it 'logs a warning and returns false when foreign_key_target is missing' do + logger = instance_spy(Logger) + allow(ForestAdminAgent::Facades::Container).to receive(:logger).and_return(logger) + + result = @decorated_bad.published?('related') + + expect(logger).to have_received(:log).with( + 'Warn', + "Field 'missing_field' (foreign_key_target) not found in schema of collection 'person'. " \ + 'This relation will be hidden. Check if the field exists in your database.' + ) + expect(result).to be(false) + end + end end end 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 24f5220c2..2fe438173 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 @@ -129,14 +129,14 @@ module Publication context 'when keep_collections_matching is called' do it 'throws an error if a name is unknown' do - expect { @datasource_decorator.keep_collections_matching(['unknown']) }.to raise_error(ForestException, 'Collection unknown not found.') - expect { @datasource_decorator.keep_collections_matching(nil, ['unknown']) }.to raise_error(ForestException, 'Collection unknown not found.') + expect { @datasource_decorator.keep_collections_matching(['unknown']) }.to raise_error(ForestException, /Collection unknown not found\./) + 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" + /Cannot remove user because it's a potential target of polymorphic relation address\.addressable/ ) end @@ -151,7 +151,7 @@ module Publication it 'is able to remove "book" collection' do @datasource_decorator.keep_collections_matching(nil, ['book']) - expect { @datasource_decorator.get_collection('book') }.to raise_error(ForestException, "Collection 'book' was removed.") + expect { @datasource_decorator.get_collection('book') }.to raise_error(ForestException, /Collection 'book' was removed\./) expect(@datasource_decorator.get_collection('library_book').schema[:fields]).not_to have_key('my_book') expect(@datasource_decorator.get_collection('library').schema[:fields]).not_to have_key('my_books') end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb index ebb8cd72e..ea91c8a89 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/datasource_decorator.rb @@ -23,6 +23,8 @@ def live_query_connections def get_collection(name) collection = @child_datasource.get_collection(name) + return nil if collection.nil? + unless @decorators.key?(collection.name) @decorators[collection.name] = @collection_decorator_class.new(collection, self) 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 3e5e84c1a..edf12d190 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 @@ -8,7 +8,13 @@ class Collection def self.get_inverse_relation(collection, relation_name) relation_field = collection.schema[:fields][relation_name] + + return nil if relation_field.nil? + foreign_collection = collection.datasource.get_collection(relation_field.foreign_collection) + + return nil if foreign_collection.nil? + polymorphic_relations = %w[PolymorphicOneToOne PolymorphicOneToMany] inverse = foreign_collection.schema[:fields].select do |_name, field| @@ -101,6 +107,9 @@ def self.get_through_target(collection, relation_name) raise ForestException, 'Relation must be many to many' unless relation.is_a?(ManyToManySchema) through_collection = collection.datasource.get_collection(relation.through_collection) + + return nil if through_collection.nil? + through_collection.schema[:fields].select do |field_name, field| if field.is_a?(ManyToOneSchema) && field.foreign_collection == relation.foreign_collection && @@ -118,6 +127,9 @@ def self.get_through_origin(collection, relation_name) raise ForestException, 'Relation must be many to many' unless relation.is_a?(ManyToManySchema) through_collection = collection.datasource.get_collection(relation.through_collection) + + return nil if through_collection.nil? + through_collection.schema[:fields].select do |field_name, field| if field.is_a?(ManyToOneSchema) && field.foreign_collection == collection.name && @@ -132,8 +144,18 @@ def self.get_through_origin(collection, relation_name) def self.list_relation(collection, primary_key_values, relation_name, caller, foreign_filter, projection) relation = collection.schema[:fields][relation_name] + + if relation.nil? + raise ForestException, "Relation '#{relation_name}' not found in collection '#{collection.name}'" + end + foreign_collection = collection.datasource.get_collection(relation.foreign_collection) + if foreign_collection.nil? + raise ForestException, + "Foreign collection '#{relation.foreign_collection}' not found for relation '#{collection.name}.#{relation_name}'" + end + if relation.is_a?(ManyToManySchema) && foreign_filter.nestable? foreign_relation = get_through_target(collection, relation_name) @@ -158,8 +180,18 @@ def self.list_relation(collection, primary_key_values, relation_name, caller, fo def self.aggregate_relation(collection, primary_key_values, relation_name, caller, foreign_filter, aggregation, limit = nil) relation = collection.schema[:fields][relation_name] + + if relation.nil? + raise ForestException, "Relation '#{relation_name}' not found in collection '#{collection.name}'" + end + foreign_collection = collection.datasource.get_collection(relation.foreign_collection) + if foreign_collection.nil? + raise ForestException, + "Foreign collection '#{relation.foreign_collection}' not found for relation '#{collection.name}.#{relation_name}'" + end + if relation.is_a?(ManyToManySchema) && foreign_filter.nestable? foreign_relation = get_through_target(collection, relation_name) if foreign_relation 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 6fa476765..324e7ec60 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 @@ -325,6 +325,41 @@ module Utils expect(described_class.aggregate_relation(collection_book, [1], 'myPersons', caller, ForestAdminDatasourceToolkit::Components::Query::Filter.new, ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count'))).to eq(1) end + + context 'when relation or collection is missing' do + it 'get_inverse_relation returns nil when relation_field is nil' do + # ForestAdminAgent logger is mocked at integration level + # Here we just test the return value + result = described_class.get_inverse_relation(collection_book, 'nonexistent_relation') + expect(result).to be_nil + end + + it 'list_relation raises ForestException when relation is nil' do + expect do + described_class.list_relation( + collection_book, + [1], + 'nonexistent_relation', + caller, + ForestAdminDatasourceToolkit::Components::Query::Filter.new, + ForestAdminDatasourceToolkit::Components::Query::Projection.new + ) + end.to raise_error(ForestException, "Relation 'nonexistent_relation' not found in collection 'Book'") + end + + it 'aggregate_relation raises ForestException when relation is nil' do + expect do + described_class.aggregate_relation( + collection_book, + [1], + 'nonexistent_relation', + caller, + ForestAdminDatasourceToolkit::Components::Query::Filter.new, + ForestAdminDatasourceToolkit::Components::Query::Aggregation.new(operation: 'Count') + ) + end.to raise_error(ForestException, "Relation 'nonexistent_relation' not found in collection 'Book'") + end + end end end end