From 097bd8e500d19ff4a8f983a9220b981bbd643ff7 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 6 Feb 2026 10:15:05 +0100 Subject: [PATCH 1/4] feat: add rake task to generate schema without server --- .rubocop.yml | 1 + .../builder/agent_factory.rb | 21 +++- .../builder/agent_factory_spec.rb | 108 ++++++++++++++++++ .../lib/tasks/forest_admin_rails_tasks.rake | 49 +++++++- 4 files changed, 174 insertions(+), 5 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 41ea6784f..85ffd82cb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -308,6 +308,7 @@ Metrics/BlockLength: - 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb' - 'packages/forest_admin_datasource_mongoid/lib/forest_admin_datasource_mongoid/collection.rb' + - 'packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake' - 'packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/sse.rb' - 'packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/http/router.rb' diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index d26118251..b2e67ad6c 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -11,6 +11,7 @@ class AgentFactory include ForestAdminDatasourceCustomizer::DSL::DatasourceHelpers attr_reader :customizer, :container, :has_env_secret + attr_accessor :schema_only_mode, :schema_output_path def setup(options) @options = options @@ -55,7 +56,25 @@ def build # Reset route cache to ensure routes are computed with all customizations ForestAdminAgent::Http::Router.reset_cached_routes! - send_schema + if @schema_only_mode + generate_schema_only(output_path: @schema_output_path) + else + send_schema + end + end + + # Generates the schema file without sending it to the Forest Admin API. + # @param output_path [String, nil] Optional custom path for the schema file + # @return [Hash] The generated schema + def generate_schema_only(output_path: nil) + datasource = @container.resolve(:datasource) + schema = build_schema(datasource) + schema_path = output_path || Facades::Container.cache(:schema_path) + + write_schema_file(schema_path, schema) + @logger.log('Info', "[ForestAdmin] Schema generated successfully at #{schema_path}") + + schema end def reload! diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb index 945cefcf1..378c0c82f 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb @@ -47,6 +47,114 @@ module Builder expect(described_class.instance.container.resolve(:datasource)) .to eq(described_class.instance.customizer.datasource({})) end + + it 'calls send_schema when schema_only_mode is false' do + instance = described_class.instance + instance.schema_only_mode = false + allow(instance).to receive(:send_schema) + + instance.build + + expect(instance).to have_received(:send_schema) + end + + it 'calls generate_schema_only when schema_only_mode is true' do + instance = described_class.instance + instance.schema_only_mode = true + allow(instance).to receive(:generate_schema_only) + + instance.build + + expect(instance).to have_received(:generate_schema_only).with(output_path: nil) + ensure + instance.schema_only_mode = false + end + + it 'passes schema_output_path to generate_schema_only' do + instance = described_class.instance + instance.schema_only_mode = true + instance.schema_output_path = '/custom/path/schema.json' + allow(instance).to receive(:generate_schema_only) + + instance.build + + expect(instance).to have_received(:generate_schema_only).with(output_path: '/custom/path/schema.json') + ensure + instance.schema_only_mode = false + instance.schema_output_path = nil + end + end + + describe 'generate_schema_only' do + it 'generates schema and writes to default path' do + instance = described_class.instance + logger = instance_spy(Services::LoggerService) + instance.instance_variable_set(:@logger, logger) + + datasource = instance_double(ForestAdminDatasourceToolkit::Datasource) + instance.container.register(:datasource, datasource) + + allow(Facades::Container).to receive(:cache).with(:schema_path).and_return('/path/to/schema.json') + allow(ForestAdminAgent::Utils::Schema::SchemaEmitter).to receive_messages(generate: [], meta: {}) + allow(File).to receive(:write) + + instance.generate_schema_only + + expect(File).to have_received(:write).with('/path/to/schema.json', anything) + expect(logger).to have_received(:log).with('Info', '[ForestAdmin] Schema generated successfully at /path/to/schema.json') + end + + it 'writes to custom output_path when provided' do + instance = described_class.instance + logger = instance_spy(Services::LoggerService) + instance.instance_variable_set(:@logger, logger) + + datasource = instance_double(ForestAdminDatasourceToolkit::Datasource) + instance.container.register(:datasource, datasource) + + allow(ForestAdminAgent::Utils::Schema::SchemaEmitter).to receive_messages(generate: [], meta: {}) + allow(File).to receive(:write) + + instance.generate_schema_only(output_path: '/custom/output.json') + + expect(File).to have_received(:write).with('/custom/output.json', anything) + expect(logger).to have_received(:log).with('Info', '[ForestAdmin] Schema generated successfully at /custom/output.json') + end + + it 'returns the generated schema' do + instance = described_class.instance + logger = instance_spy(Services::LoggerService) + instance.instance_variable_set(:@logger, logger) + + datasource = instance_double(ForestAdminDatasourceToolkit::Datasource) + instance.container.register(:datasource, datasource) + + allow(Facades::Container).to receive(:cache).with(:schema_path).and_return('/path/to/schema.json') + allow(ForestAdminAgent::Utils::Schema::SchemaEmitter).to receive_messages(generate: [{ name: 'Book' }], meta: { liana: 'forest-rails' }) + allow(File).to receive(:write) + + result = instance.generate_schema_only + + expect(result).to eq({ meta: { liana: 'forest-rails' }, collections: [{ name: 'Book' }] }) + end + + it 'does not call post_schema' do + instance = described_class.instance + logger = instance_spy(Services::LoggerService) + instance.instance_variable_set(:@logger, logger) + + datasource = instance_double(ForestAdminDatasourceToolkit::Datasource) + instance.container.register(:datasource, datasource) + + allow(Facades::Container).to receive(:cache).with(:schema_path).and_return('/path/to/schema.json') + allow(ForestAdminAgent::Utils::Schema::SchemaEmitter).to receive_messages(generate: [], meta: {}) + allow(File).to receive(:write) + allow(instance).to receive(:post_schema) + + instance.generate_schema_only + + expect(instance).not_to have_received(:post_schema) + end end describe 'reload!' do diff --git a/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake b/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake index e3b89673d..54fc573e8 100644 --- a/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake +++ b/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake @@ -1,4 +1,45 @@ -# desc "Explaining what the task does" -# task :forest_admin_rails do -# # Task goes here -# end +namespace :forest_admin do + namespace :schema do + desc 'Generate the Forest Admin schema file without starting the server or sending it to the API' + task generate: :environment do + require 'forest_admin_agent' + + output_path = ENV.fetch('output', nil) + debug_mode = ENV['debug'] == 'true' + + puts '[ForestAdmin] Starting schema generation...' + + # Force eager loading of all models + Rails.application.eager_load! + + # Check if create_agent.rb exists + create_agent_path = Rails.root.join('lib', 'forest_admin_rails', 'create_agent.rb') + unless File.exist?(create_agent_path) + puts '[ForestAdmin] Error: create_agent.rb not found at lib/forest_admin_rails/create_agent.rb' + puts '[ForestAdmin] Run `rails generate forest_admin_rails ` to create it.' + exit 1 + end + + # Setup the agent factory + agent_factory = ForestAdminAgent::Builder::AgentFactory.instance + agent_factory.setup(ForestAdminRails.config) + + # Enable schema-only mode (generates schema without sending to API) + agent_factory.schema_only_mode = true + agent_factory.schema_output_path = output_path if output_path + + # Load and execute the create_agent.rb file + require create_agent_path + + # Call setup! which will generate the schema + begin + ForestAdminRails::CreateAgent.setup! + puts '[ForestAdmin] Schema generation completed!' + rescue StandardError => e + puts "[ForestAdmin] Error during schema generation: #{e.message}" + puts e.backtrace.first(10).join("\n") if debug_mode + exit 1 + end + end + end +end From 6ae4db29b1bc2db34b87dd097b2c3aeac52f7b82 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 6 Feb 2026 16:48:22 +0100 Subject: [PATCH 2/4] chore: remove comment Co-authored-by: dogan.ay <65234588+DayTF@users.noreply.github.com> --- .../lib/forest_admin_agent/builder/agent_factory.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index b2e67ad6c..04d02b361 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -63,9 +63,6 @@ def build end end - # Generates the schema file without sending it to the Forest Admin API. - # @param output_path [String, nil] Optional custom path for the schema file - # @return [Hash] The generated schema def generate_schema_only(output_path: nil) datasource = @container.resolve(:datasource) schema = build_schema(datasource) From 029fc7c508beac48f0116233c271312e71e9e5a1 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 6 Feb 2026 17:03:18 +0100 Subject: [PATCH 3/4] feat: remove output option from schema generate command --- .../builder/agent_factory.rb | 10 +++--- .../builder/agent_factory_spec.rb | 33 +------------------ .../lib/tasks/forest_admin_rails_tasks.rake | 2 -- 3 files changed, 7 insertions(+), 38 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index 04d02b361..6bd60ec40 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -11,7 +11,7 @@ class AgentFactory include ForestAdminDatasourceCustomizer::DSL::DatasourceHelpers attr_reader :customizer, :container, :has_env_secret - attr_accessor :schema_only_mode, :schema_output_path + attr_accessor :schema_only_mode def setup(options) @options = options @@ -57,16 +57,18 @@ def build ForestAdminAgent::Http::Router.reset_cached_routes! if @schema_only_mode - generate_schema_only(output_path: @schema_output_path) + generate_schema_only else send_schema end end - def generate_schema_only(output_path: nil) + # Generates the schema file without sending it to the Forest Admin API. + # @return [Hash] The generated schema + def generate_schema_only datasource = @container.resolve(:datasource) schema = build_schema(datasource) - schema_path = output_path || Facades::Container.cache(:schema_path) + schema_path = Facades::Container.cache(:schema_path) write_schema_file(schema_path, schema) @logger.log('Info', "[ForestAdmin] Schema generated successfully at #{schema_path}") diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb index 378c0c82f..ee1cc84f8 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/builder/agent_factory_spec.rb @@ -65,24 +65,10 @@ module Builder instance.build - expect(instance).to have_received(:generate_schema_only).with(output_path: nil) + expect(instance).to have_received(:generate_schema_only) ensure instance.schema_only_mode = false end - - it 'passes schema_output_path to generate_schema_only' do - instance = described_class.instance - instance.schema_only_mode = true - instance.schema_output_path = '/custom/path/schema.json' - allow(instance).to receive(:generate_schema_only) - - instance.build - - expect(instance).to have_received(:generate_schema_only).with(output_path: '/custom/path/schema.json') - ensure - instance.schema_only_mode = false - instance.schema_output_path = nil - end end describe 'generate_schema_only' do @@ -104,23 +90,6 @@ module Builder expect(logger).to have_received(:log).with('Info', '[ForestAdmin] Schema generated successfully at /path/to/schema.json') end - it 'writes to custom output_path when provided' do - instance = described_class.instance - logger = instance_spy(Services::LoggerService) - instance.instance_variable_set(:@logger, logger) - - datasource = instance_double(ForestAdminDatasourceToolkit::Datasource) - instance.container.register(:datasource, datasource) - - allow(ForestAdminAgent::Utils::Schema::SchemaEmitter).to receive_messages(generate: [], meta: {}) - allow(File).to receive(:write) - - instance.generate_schema_only(output_path: '/custom/output.json') - - expect(File).to have_received(:write).with('/custom/output.json', anything) - expect(logger).to have_received(:log).with('Info', '[ForestAdmin] Schema generated successfully at /custom/output.json') - end - it 'returns the generated schema' do instance = described_class.instance logger = instance_spy(Services::LoggerService) diff --git a/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake b/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake index 54fc573e8..c6f5ec581 100644 --- a/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake +++ b/packages/forest_admin_rails/lib/tasks/forest_admin_rails_tasks.rake @@ -4,7 +4,6 @@ namespace :forest_admin do task generate: :environment do require 'forest_admin_agent' - output_path = ENV.fetch('output', nil) debug_mode = ENV['debug'] == 'true' puts '[ForestAdmin] Starting schema generation...' @@ -26,7 +25,6 @@ namespace :forest_admin do # Enable schema-only mode (generates schema without sending to API) agent_factory.schema_only_mode = true - agent_factory.schema_output_path = output_path if output_path # Load and execute the create_agent.rb file require create_agent_path From 41ce340398b2b537ecc3b0f280c622ab377ec94c Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 6 Feb 2026 17:40:19 +0100 Subject: [PATCH 4/4] chore: remove comment --- .../lib/forest_admin_agent/builder/agent_factory.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb index 6bd60ec40..4255653c4 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/builder/agent_factory.rb @@ -63,8 +63,6 @@ def build end end - # Generates the schema file without sending it to the Forest Admin API. - # @return [Hash] The generated schema def generate_schema_only datasource = @container.resolve(:datasource) schema = build_schema(datasource)