From c6383b54e2d590b7e5bd87ada48695c5a36b4bc8 Mon Sep 17 00:00:00 2001 From: Olivier Bex-Chauvet Date: Fri, 16 Oct 2026 16:11:20 +0200 Subject: [PATCH 1/2] fix(datasource_active_record): skip anonymous models when building collections The audit trail binds its table through an anonymous ActiveRecord class, and the datasource called `name.start_with?` on every ActiveRecord::Base descendant, so a Rails app with the audit trail on crashed at boot on `nil`. A nameless model can't name a collection, so it is skipped, which also keeps the audit table out of the schema. Refs: PRD-1668 Co-Authored-By: Claude Opus 5.5 --- .../datasource.rb | 3 +++ .../datasource_spec.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+) 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 886b47502..631c5e6e1 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 @@ -76,6 +76,9 @@ def primary_key?(model) end def fetch_model(model) + # A collection is named after its model, so an anonymous one (such as the audit trail's) can't be one. + return if model.name.nil? + if model.name.start_with?('HABTM_') build_habtm(model) else 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 57b575140..849ff8192 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 @@ -15,6 +15,21 @@ module ForestAdminDatasourceActiveRecord expect(collections).to all(be_a(String)) end + describe 'with an anonymous model' do + let!(:anonymous_model) { Class.new(ActiveRecord::Base) { self.table_name = 'accounts' } } + + it 'boots instead of failing on the nameless model' do + expect { described_class.new({ adapter: 'sqlite3', database: 'db/database.db' }) }.not_to raise_error + end + + it 'leaves the nameless model out of the collections and keeps the named ones' do + datasource = described_class.new({ adapter: 'sqlite3', database: 'db/database.db' }) + + expect(datasource.collections.values.map(&:model)).not_to include(anonymous_model) + expect(datasource.collections.keys).to include('Account') + end + end + describe '#init_orm' do it 'uses connection_pool.db_config instead of deprecated connection_db_config' do mock_db_config = instance_double(ActiveRecord::DatabaseConfigurations::HashConfig, env_name: 'test') From 5a99639928c3f66998b2ccc95744c356fa513ee5 Mon Sep 17 00:00:00 2001 From: Olivier Bex-Chauvet Date: Fri, 16 Oct 2026 17:01:33 +0200 Subject: [PATCH 2/2] test(audit_trail): pin the audit model as anonymous The ActiveRecord datasource now skips nameless models, and that is all that keeps the audit table out of the schema, so a spec fails if the store ever names its model. Refs: PRD-1668 Co-Authored-By: Claude Opus 5.5 --- .../spec/lib/forest_admin_agent/audit_trail/store_spec.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/audit_trail/store_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/audit_trail/store_spec.rb index a76ba91b9..4be73c167 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/audit_trail/store_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/audit_trail/store_spec.rb @@ -274,6 +274,11 @@ def timeline(**query) .to eq(%w[main other]) end + # The ActiveRecord datasource skips nameless models: a name would expose the audit table as a collection. + it 'keeps its model anonymous' do + expect(store.send(:model).name).to be_nil + end + it 'lists only entries whose diff touched one of the given fields' do store.append(record(previous_values: { 'status' => 'open' }, new_values: { 'status' => 'closed' })) store.append(record(previous_values: { 'note' => 'a' }, new_values: { 'note' => 'b' }))