Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions react_on_rails/Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ target :lib do
check "lib/react_on_rails.rb"
check "lib/react_on_rails/configuration.rb"
check "lib/react_on_rails/controller.rb"
check "lib/react_on_rails/dev/database_checker.rb"
check "lib/react_on_rails/dev/file_manager.rb"
check "lib/react_on_rails/dev/pack_generator.rb"
check "lib/react_on_rails/dev/process_manager.rb"
Expand Down
116 changes: 116 additions & 0 deletions react_on_rails/lib/react_on_rails/dev/database_checker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# frozen_string_literal: true

require "rainbow"

module ReactOnRails
module Dev
# DatabaseChecker validates that the database is set up before starting
# the development server.
#
# This prevents confusing errors buried in combined Foreman/Overmind logs
# and provides clear guidance on how to set up the database.
#
class DatabaseChecker
class << self
# Check if the database is set up and provide helpful output
#
# @return [Boolean] true if database is ready, false otherwise
def check_database
return true unless rails_available?

check_and_report_database
end

private

def rails_available?
defined?(Rails) && defined?(ActiveRecord::Base)
end

def check_and_report_database
print_check_header

result = database_ready?
if result[:ready]
print_database_ok
true
else
print_database_failed(result[:error_type], result[:error_message])
false
end
end

def database_ready?
# Try to establish connection and run a simple query
ActiveRecord::Base.connection.execute("SELECT 1")
{ ready: true }
rescue ActiveRecord::NoDatabaseError
# Database doesn't exist
{ ready: false, error_type: :no_database }
rescue ActiveRecord::PendingMigrationError
# Database exists but migrations are pending
{ ready: false, error_type: :pending_migrations }
rescue ActiveRecord::ConnectionNotEstablished,
ActiveRecord::StatementInvalid => e
# Connection failed or other database error
{ ready: false, error_type: :connection_error, error_message: e.message }
rescue StandardError => e
# Unexpected error - log but don't block startup
# This allows apps without databases to still use bin/dev
warn "Database check warning: #{e.message}" if ENV["DEBUG"]
{ ready: true }
end

def print_check_header
puts ""
puts Rainbow("🗄️ Checking database...").cyan.bold
puts ""
end

def print_database_ok
puts " #{Rainbow('✓').green} Database is ready"
puts ""
end

# rubocop:disable Metrics/AbcSize
def print_database_failed(error_type, error_message)
puts " #{Rainbow('✗').red} Database is not ready"
puts ""
puts Rainbow("❌ Database not set up!").red.bold
puts ""

case error_type
when :no_database
puts Rainbow("The database does not exist.").yellow
puts ""
puts Rainbow("Run one of these commands:").cyan.bold
puts " #{Rainbow('bin/rails db:setup').green} # Create database, load schema, seed data"
puts " #{Rainbow('bin/rails db:create').green} # Just create the database"
when :pending_migrations
puts Rainbow("The database exists but has pending migrations.").yellow
puts ""
puts Rainbow("Run this command:").cyan.bold
puts " #{Rainbow('bin/rails db:migrate').green} # Run pending migrations"
when :connection_error
puts Rainbow("Could not connect to the database.").yellow
if error_message
puts ""
puts Rainbow("Error: #{error_message}").red
end
puts ""
puts Rainbow("Possible solutions:").cyan.bold
puts " #{Rainbow('1.').yellow} Check if your database server is running"
puts " #{Rainbow('2.').yellow} Verify database.yml configuration"
puts " #{Rainbow('3.').yellow} Run #{Rainbow('bin/rails db:setup').green} to create the database"
end

puts ""
puts Rainbow("💡 Tip:").blue.bold
puts " After fixing the database, run #{Rainbow('bin/dev').green} again"
puts ""
end
# rubocop:enable Metrics/AbcSize
end
end
end
end
10 changes: 7 additions & 3 deletions react_on_rails/lib/react_on_rails/dev/server_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "open3"
require "rainbow"
require_relative "../packer_utils"
require_relative "database_checker"
require_relative "service_checker"

module ReactOnRails
Expand Down Expand Up @@ -411,8 +412,9 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil)

print_procfile_info(procfile, route: route)

# Check required services before starting
# Check required services and database before starting
exit 1 unless ServiceChecker.check_services
exit 1 unless DatabaseChecker.check_database

print_server_info(
"🏭 Starting production-like development server...",
Expand Down Expand Up @@ -536,8 +538,9 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil)
def run_static_development(procfile, verbose: false, route: nil)
print_procfile_info(procfile, route: route)

# Check required services before starting
# Check required services and database before starting
exit 1 unless ServiceChecker.check_services
exit 1 unless DatabaseChecker.check_database

features = [
"Using shakapacker --watch (no HMR)",
Expand Down Expand Up @@ -565,8 +568,9 @@ def run_static_development(procfile, verbose: false, route: nil)
def run_development(procfile, verbose: false, route: nil)
print_procfile_info(procfile, route: route)

# Check required services before starting
# Check required services and database before starting
exit 1 unless ServiceChecker.check_services
exit 1 unless DatabaseChecker.check_database

PackGenerator.generate(verbose: verbose)
ProcessManager.ensure_procfile(procfile)
Expand Down
16 changes: 16 additions & 0 deletions react_on_rails/sig/react_on_rails/dev/database_checker.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module ReactOnRails
module Dev
class DatabaseChecker
def self.check_database: () -> bool

private

def self.rails_available?: () -> bool
def self.check_and_report_database: () -> bool
def self.database_ready?: () -> Hash[Symbol, untyped]
def self.print_check_header: () -> void
def self.print_database_ok: () -> void
def self.print_database_failed: (Symbol?, String?) -> void
end
end
end
161 changes: 161 additions & 0 deletions react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# frozen_string_literal: true

require "react_on_rails/dev/database_checker"
require "stringio"

# Create a test helper class for mocking ActiveRecord connection
class MockConnection
def execute(query); end
end

# Create a test helper class for mocking ActiveRecord::Base
class MockActiveRecordBase
def self.connection; end
end

RSpec.describe ReactOnRails::Dev::DatabaseChecker do
describe ".check_database" do
context "when Rails/ActiveRecord is not available" do
before do
hide_const("Rails")
hide_const("ActiveRecord::Base")
end

it "returns true without checking database" do
expect(described_class.check_database).to be true
end
end

context "when Rails is available" do
let(:mock_connection) { instance_double(MockConnection) }

before do
stub_const("Rails", class_double(Object))
stub_const("ActiveRecord::Base", class_double(MockActiveRecordBase, connection: mock_connection))
stub_const("ActiveRecord::NoDatabaseError", Class.new(StandardError))
stub_const("ActiveRecord::PendingMigrationError", Class.new(StandardError))
stub_const("ActiveRecord::ConnectionNotEstablished", Class.new(StandardError))
stub_const("ActiveRecord::StatementInvalid", Class.new(StandardError))
end

context "when database is ready" do
before do
allow(mock_connection).to receive(:execute).with("SELECT 1").and_return(true)
end

it "returns true" do
output = capture_stdout do
expect(described_class.check_database).to be true
end

expect(output).to include("Checking database")
expect(output).to include("Database is ready")
end
end

context "when database does not exist" do
before do
allow(mock_connection).to receive(:execute).and_raise(ActiveRecord::NoDatabaseError)
end

it "returns false and shows db:setup guidance" do
output = capture_stdout do
expect(described_class.check_database).to be false
end

expect(output).to include("Database not set up")
expect(output).to include("database does not exist")
expect(output).to include("bin/rails db:setup")
expect(output).to include("bin/rails db:create")
end
end

context "when migrations are pending" do
before do
allow(mock_connection).to receive(:execute).and_raise(ActiveRecord::PendingMigrationError)
end

it "returns false and shows db:migrate guidance" do
output = capture_stdout do
expect(described_class.check_database).to be false
end

expect(output).to include("Database not set up")
expect(output).to include("pending migrations")
expect(output).to include("bin/rails db:migrate")
end
end

context "when connection cannot be established" do
before do
allow(mock_connection).to receive(:execute).and_raise(
ActiveRecord::ConnectionNotEstablished.new("Connection refused")
)
end

it "returns false and shows connection troubleshooting" do
output = capture_stdout do
expect(described_class.check_database).to be false
end

expect(output).to include("Database not set up")
expect(output).to include("Could not connect")
expect(output).to include("database server is running")
expect(output).to include("database.yml")
end
end

context "when a statement error occurs" do
before do
allow(mock_connection).to receive(:execute).and_raise(
ActiveRecord::StatementInvalid.new("Unknown error")
)
end

it "returns false and shows connection troubleshooting" do
output = capture_stdout do
expect(described_class.check_database).to be false
end

expect(output).to include("Database not set up")
expect(output).to include("Could not connect")
end
end

context "when an unexpected error occurs" do
before do
allow(mock_connection).to receive(:execute).and_raise(StandardError.new("Something unexpected"))
end

it "returns true to allow apps without databases to continue" do
output = capture_stdout do
expect(described_class.check_database).to be true
end

# Should show the check header but not fail
expect(output).to include("Checking database")
end

it "outputs a warning when DEBUG is enabled" do
allow(ENV).to receive(:[]).with("DEBUG").and_return("true")
expect { described_class.check_database }.to output(/Database check warning/).to_stderr
end

it "does not output a warning when DEBUG is not enabled" do
allow(ENV).to receive(:[]).with("DEBUG").and_return(nil)
expect { described_class.check_database }.not_to output(/Database check warning/).to_stderr
end
end
end
end

# Helper methods
def capture_stdout
old_stdout = $stdout
$stdout = StringIO.new
yield
$stdout.string
ensure
$stdout = old_stdout
end
end
Loading