Skip to content

Commit

Permalink
Merge pull request #104 from rage-rb/standalone-db-2
Browse files Browse the repository at this point in the history
Allow to preconfigure a database for new apps
  • Loading branch information
rsamoilov authored Sep 16, 2024
2 parents 9bca41f + 60c15b5 commit 4a2be60
Show file tree
Hide file tree
Showing 19 changed files with 328 additions and 14 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ AllCops:
TargetRubyVersion: 3.1
Exclude:
- vendor/bundle/**/*
- lib/rage/templates/**/*
DisabledByDefault: true
SuggestExtensions: false

Expand Down
5 changes: 5 additions & 0 deletions lib/rage-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def self.patch_active_record_connection_pool
end
end

def self.load_tasks
Rage::Tasks.init
end

# @private
def self.with_middlewares(app, middlewares)
middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
Expand Down Expand Up @@ -113,6 +117,7 @@ module ActiveRecord
end
end

autoload :Tasks, "rage/tasks"
autoload :Cookies, "rage/cookies"
autoload :Session, "rage/session"
autoload :Cable, "rage/cable/cable"
Expand Down
149 changes: 140 additions & 9 deletions lib/rage/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,54 @@
require "rage/version"

module Rage
class CLICodeGenerator < Thor
include Thor::Actions

def self.source_root
File.expand_path("templates", __dir__)
end

desc "migration NAME", "Generate a new migration."
def migration(name = nil)
return help("migration") if name.nil?

setup
Rake::Task["db:new_migration"].invoke(name)
end

desc "model NAME", "Generate a new model."
def model(name = nil)
return help("model") if name.nil?

setup
migration("create_#{name.pluralize}")
@model_name = name.classify
template("model-template/model.rb", "app/models/#{name.singularize.underscore}.rb")
end

private

def setup
@setup ||= begin
require "rake"
load "Rakefile"
end
end
end

class CLI < Thor
def self.exit_on_failure?
true
end

desc "new PATH", "Create a new application."
def new(path)
option :database, aliases: "-d", desc: "Preconfigure for selected database.", enum: %w(mysql trilogy postgresql sqlite3)
option :help, aliases: "-h", desc: "Show this message."
def new(path = nil)
return help("new") if options.help? || path.nil?

require "rage/all"
NewAppGenerator.start([path])
CLINewAppGenerator.start([path, options[:database]])
end

desc "s", "Start the app server."
Expand Down Expand Up @@ -130,6 +169,43 @@ def version
puts Rage::VERSION
end

map "generate" => :g
desc "g TYPE", "Generate new code."
subcommand "g", CLICodeGenerator

map "--tasks" => :tasks
desc "--tasks", "See the list of available tasks."
def tasks
require "io/console"

tasks = linked_rake_tasks
return if tasks.empty?

_, max_width = IO.console.winsize
max_task_name = tasks.max_by { |task| task.name.length }.name.length + 2
max_comment = max_width - max_task_name - 8

tasks.each do |task|
comment = task.comment.length <= max_comment ? task.comment : "#{task.comment[0...max_comment - 5]}..."
puts sprintf("rage %-#{max_task_name}s # %s", task.name, comment)
end
end

def method_missing(method_name, *, &)
set_env({})

if respond_to?(method_name)
Rake::Task[method_name].invoke
else
suggestions = linked_rake_tasks.map(&:name)
raise UndefinedCommandError.new(method_name.to_s, suggestions, nil)
end
end

def respond_to_missing?(method_name, include_private = false)
linked_rake_tasks.any? { |task| task.name == method_name.to_s } || super
end

private

def environment
Expand All @@ -141,30 +217,85 @@ def environment
end

def set_env(options)
ENV["RAGE_ENV"] = options[:environment] if options[:environment]
if options[:environment]
ENV["RAGE_ENV"] = ENV["RAILS_ENV"] = options[:environment]
elsif ENV["RAGE_ENV"]
ENV["RAILS_ENV"] = ENV["RAGE_ENV"]
elsif ENV["RAILS_ENV"]
ENV["RAGE_ENV"] = ENV["RAILS_ENV"]
else
ENV["RAGE_ENV"] = ENV["RAILS_ENV"] = "development"
end
end

def linked_rake_tasks
require "rake"
Rake::TaskManager.record_task_metadata = true
load "Rakefile"

# at this point we don't know whether the app is running in standalone or Rails mode;
# we set both variables to make sure applications are running in the same environment;
ENV["RAILS_ENV"] = ENV["RAGE_ENV"] if ENV["RAGE_ENV"] && ENV["RAILS_ENV"] != ENV["RAGE_ENV"]
Rake::Task.tasks.select { |task| !task.comment.nil? && task.name.start_with?("db:") }
end
end

class NewAppGenerator < Thor::Group
class CLINewAppGenerator < Thor::Group
include Thor::Actions
argument :path, type: :string
argument :database, type: :string, required: false

def self.source_root
File.expand_path("templates", __dir__)
end

def setup
@use_database = !database.nil?
end

def create_directory
empty_directory(path)
end

def copy_files
Dir.glob("*", base: self.class.source_root).each do |template|
inject_templates
end

def install_database
return unless @use_database

@app_name = path.tr("-", "_").downcase
append_to_file "#{path}/Gemfile", <<~RUBY
gem "#{get_db_gem_name}"
gem "activerecord"
gem "standalone_migrations", require: false
RUBY

inject_templates("db-templates")
inject_templates("db-templates/#{database}")
end

private

def inject_templates(from = nil)
root = "#{self.class.source_root}/#{from}"

Dir.glob("*", base: root).each do |template|
next if File.directory?("#{root}/#{template}")

*template_path_parts, template_name = template.split("-")
template(template, "#{path}/#{template_path_parts.join("/")}/#{template_name}")
template("#{root}/#{template}", [path, *template_path_parts, template_name].join("/"))
end
end

def get_db_gem_name
case database
when "mysql"
"mysql2"
when "trilogy"
"trilogy"
when "postgresql"
"pg"
when "sqlite3"
"sqlite3"
end
end
end
Expand Down
39 changes: 38 additions & 1 deletion lib/rage/ext/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,44 @@ def connection_cache_key(_)
end
end

# connect to the database in standalone mode
database_url, database_file = ENV["DATABASE_URL"], Rage.root.join("config/database.yml")
if defined?(ActiveRecord) && !Rage.config.internal.rails_mode && (database_url || database_file.exist?)
# transform database URL to an object
database_url_config = if database_url.nil?
{}
elsif ActiveRecord.version >= Gem::Version.create("6.1.0")
ActiveRecord::Base.configurations
ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.new(database_url).to_hash
else
ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(database_url).to_hash
end
database_url_config.transform_keys!(&:to_s)

# load config/database.yml
if database_file.exist?
database_file_config = begin
require "yaml"
require "erb"
YAML.safe_load(ERB.new(database_file.read).result, aliases: true)
end

# merge database URL config into the file config (only if we have one database)
database_file_config.transform_values! do |env_config|
env_config.all? { |_, v| v.is_a?(Hash) } ? env_config : env_config.merge(database_url_config)
end
end

ActiveRecord::Base.configurations = database_file_config || { Rage.env.to_s => database_url_config }
ActiveRecord::Base.establish_connection(Rage.env.to_sym)

unless defined?(Rake)
ActiveRecord::Base.logger = Rage.logger if Rage.logger.debug?
ActiveRecord::Base.connection_pool.with_connection {} # validate the connection
end
end

# patch `ActiveRecord::ConnectionPool`
if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
if defined?(ActiveRecord) && !defined?(Rake) && Rage.config.internal.patch_ar_pool?
Rage.patch_active_record_connection_pool
end
2 changes: 1 addition & 1 deletion lib/rage/logger/logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def initialize(log, level: Logger::DEBUG, formatter: Rage::TextFormatter.new, sh
end

@formatter = formatter
@level = level
@level = @logdev ? level : Logger::UNKNOWN
define_log_methods
end

Expand Down
4 changes: 2 additions & 2 deletions lib/rage/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
# Run application initializers
Dir["#{Rage.root}/config/initializers/**/*.rb"].each { |initializer| load(initializer) }

require "rage/ext/setup"

# Load application classes
Rage.code_loader.setup

require_relative "#{Rage.root}/config/routes"

require "rage/ext/setup"
33 changes: 33 additions & 0 deletions lib/rage/tasks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
begin
require "standalone_migrations"
rescue LoadError
end

class Rage::Tasks
class << self
def init
load_db_tasks if defined?(StandaloneMigrations)
end

private

def load_db_tasks
StandaloneMigrations::Configurator.prepend(Module.new do
def configuration_file
@path ||= begin
@__tempfile = Tempfile.new
@__tempfile.write <<~YAML
config:
database: config/database.yml
YAML
@__tempfile.close

@__tempfile.path
end
end
end)

StandaloneMigrations::Tasks.load_tasks
end
end
end
1 change: 1 addition & 0 deletions lib/rage/templates/Rakefile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
require_relative "config/application"
Rage.load_tasks
3 changes: 3 additions & 0 deletions lib/rage/templates/config-application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
require "rage"
Bundler.require(*Rage.groups)

<% if @use_database -%>
require "active_record"
<% end -%>
require "rage/all"
Rage.configure do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
9 changes: 9 additions & 0 deletions lib/rage/templates/db-templates/db-seeds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This file should ensure the existence of records required to run the application in every environment (production,
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Example:
#
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
# MovieGenre.find_or_create_by!(name: genre_name)
# end
24 changes: 24 additions & 0 deletions lib/rage/templates/db-templates/mysql/config-database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
##
# MySQL. Versions 5.5.8 and up are supported.
#
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%%= ENV.fetch("DB_MAX_CONNECTIONS") { 5 } %>
username: root
password:
socket: /tmp/mysql.sock

development:
<<: *default
database: <%= @app_name %>_development

test:
<<: *default
database: <%= @app_name %>_test

production:
<<: *default
database: <%= @app_name %>_production
username: <%= @app_name %>
password: <%%= ENV["<%= @app_name.upcase %>_DATABASE_PASSWORD"] %>
21 changes: 21 additions & 0 deletions lib/rage/templates/db-templates/postgresql/config-database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
##
# PostgreSQL. Versions 9.3 and up are supported.
#
default: &default
adapter: postgresql
encoding: unicode
pool: <%%= ENV.fetch("DB_MAX_CONNECTIONS") { 5 } %>

development:
<<: *default
database: <%= @app_name %>_development

test:
<<: *default
database: <%= @app_name %>_test

production:
<<: *default
database: <%= @app_name %>_production
username: <%= @app_name %>
password: <%%= ENV["<%= @app_name.upcase %>_DATABASE_PASSWORD"] %>
Loading

0 comments on commit 4a2be60

Please sign in to comment.