feat: extend cli with db_migrate and db_rollback
This commit is contained in:
1 parent
f8f799f38f
commit
c3ff02a741
1 file changed
+181
@@ -23,10 +23,191 @@ module Prism
|
|||||||
case command
|
case command
|
||||||
when 'new' then new_app(rest.first || raise(UsageError, 'usage: prism new APP_NAME'))
|
when 'new' then new_app(rest.first || raise(UsageError, 'usage: prism new APP_NAME'))
|
||||||
when 'generate', 'g' then generate(rest)
|
when 'generate', 'g' then generate(rest)
|
||||||
|
when 'jobs' then jobs(rest)
|
||||||
|
when 'assets:precompile' then assets_precompile(rest)
|
||||||
|
when 'console', 'c' then console(rest)
|
||||||
|
when 'routes' then routes(rest)
|
||||||
|
when 'db:migrate' then db_migrate(rest)
|
||||||
|
when 'db:rollback' then db_rollback(rest)
|
||||||
|
when 'db:status' then db_status(rest)
|
||||||
|
when 'db:seed' then db_seed(rest)
|
||||||
else raise UsageError, "unknown command #{command.inspect}"
|
else raise UsageError, "unknown command #{command.inspect}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Applies every pending migration directly against a PStore file, the
|
||||||
|
# same work `Orm.migrator(...).migrate!` does when a generated app.rb
|
||||||
|
# boots - but as a standalone step that does not require loading the
|
||||||
|
# rest of the application (no controllers, no server), mirroring `rails
|
||||||
|
# db:migrate`. Useful on its own for apps that do not auto-migrate at
|
||||||
|
# boot, and it is the only way to inspect/rewind migrations (db:status,
|
||||||
|
# db:rollback below) without booting an app that would immediately
|
||||||
|
# re-apply everything pending anyway.
|
||||||
|
def db_migrate(args, out: $stdout)
|
||||||
|
migrator = build_migrator(parse_db_options(args))
|
||||||
|
applied = migrator.migrate!
|
||||||
|
if applied.empty?
|
||||||
|
out.puts '[prism db:migrate] no pending migrations'
|
||||||
|
else
|
||||||
|
applied.each { |version| out.puts "[prism db:migrate] migrated #{version}" }
|
||||||
|
end
|
||||||
|
applied
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reverts the most recently applied migration(s), like `rails
|
||||||
|
# db:rollback STEP=n` - `--step=N` picks how many to undo (default 1).
|
||||||
|
def db_rollback(args, out: $stdout)
|
||||||
|
options = parse_db_options(args)
|
||||||
|
migrator = build_migrator(options)
|
||||||
|
reverted = migrator.rollback!(options[:step])
|
||||||
|
if reverted.empty?
|
||||||
|
out.puts '[prism db:rollback] nothing to roll back'
|
||||||
|
else
|
||||||
|
reverted.each { |version| out.puts "[prism db:rollback] rolled back #{version}" }
|
||||||
|
end
|
||||||
|
reverted
|
||||||
|
end
|
||||||
|
|
||||||
|
# Lists every migration file found under --migrations next to whether
|
||||||
|
# it has been applied to --database, like `rails db:migrate:status`.
|
||||||
|
def db_status(args, out: $stdout)
|
||||||
|
migrator = build_migrator(parse_db_options(args))
|
||||||
|
status = migrator.status
|
||||||
|
rows = status.map { |entry, applied| [applied ? 'up' : 'down', entry.version, entry.name] }
|
||||||
|
header = %w[STATUS VERSION NAME]
|
||||||
|
widths = header.each_index.map { |i| ([header[i].length] + rows.map { |row| row[i].length }).max }
|
||||||
|
out.puts format_row(header, widths)
|
||||||
|
rows.each { |row| out.puts format_row(row, widths) }
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
# Loads the application (via --require, same flag shape as `jobs work`/
|
||||||
|
# `console`) and then `load`s --file (default db/seeds.rb) as plain Ruby,
|
||||||
|
# like `rails db:seed`. Deliberately `load` rather than `require`: a
|
||||||
|
# seeds file has no meaningful "already loaded" state of its own (unlike
|
||||||
|
# a model or controller file, it is not a class/module definition but a
|
||||||
|
# sequence of side effects), so a second `db:seed` run - or a console/
|
||||||
|
# test process that seeds more than once - must re-execute it, not
|
||||||
|
# silently no-op the way `require` would after the first call already
|
||||||
|
# loaded that exact path.
|
||||||
|
#
|
||||||
|
# Idempotency of what the seeds file actually does (find_or_create_by
|
||||||
|
# vs. create!, etc.) is the app's concern, not this command's - same
|
||||||
|
# division of responsibility Rails draws. A missing seeds file is not an
|
||||||
|
# error: plenty of apps never have one, and `prism new` does not
|
||||||
|
# generate a stub, so failing here would make db:seed unusable as a
|
||||||
|
# step in a deploy script unless every app remembered to create an empty
|
||||||
|
# placeholder first.
|
||||||
|
def db_seed(args, out: $stdout)
|
||||||
|
options = parse_seed_options(args)
|
||||||
|
options[:require].each { |path| require File.expand_path(path, @root) }
|
||||||
|
|
||||||
|
seeds_path = File.expand_path(options[:file], @root)
|
||||||
|
unless File.exist?(seeds_path)
|
||||||
|
out.puts "[prism db:seed] no seeds file at #{options[:file]}"
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
load seeds_path
|
||||||
|
out.puts "[prism db:seed] seeded from #{options[:file]}"
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Lists every route registered on a loaded Application subclass, like
|
||||||
|
# `rails routes`: NAME, VERB, PATH and the controller#action (or
|
||||||
|
# "(block)" for routes defined inline rather than via `to:`) it
|
||||||
|
# dispatches to, in registration order (the same order the router tries
|
||||||
|
# them in, which matters when patterns overlap). Mounted Rack apps
|
||||||
|
# (`mount`) are listed separately underneath since they do not carry a
|
||||||
|
# single verb/action - only a prefix and whatever app answers under it.
|
||||||
|
def routes(args, out: $stdout)
|
||||||
|
options = parse_routes_options(args)
|
||||||
|
loaded_before = Application.subclasses
|
||||||
|
options[:require].each { |path| require File.expand_path(path, @root) }
|
||||||
|
newly_loaded = Application.subclasses - loaded_before
|
||||||
|
|
||||||
|
app_class = resolve_app_class(options[:app], newly_loaded)
|
||||||
|
print_routes(app_class.router, out)
|
||||||
|
app_class
|
||||||
|
end
|
||||||
|
|
||||||
|
# A minimal `rails console`-style REPL: loads the application (via
|
||||||
|
# --require, same flag shape as `jobs work`) and evaluates one line of
|
||||||
|
# Ruby at a time against a binding that survives across lines, so a
|
||||||
|
# local variable assigned on one line is still visible on the next -
|
||||||
|
# e.g. `post = Post.first` then `post.title`. Deliberately single-line
|
||||||
|
# (no heredoc/multi-line statement support) and has no history/tab
|
||||||
|
# completion, unlike a real irb - this is a REPL, not a terminal editor;
|
||||||
|
# it reads whatever `input` yields line by line, which makes it just as
|
||||||
|
# easy to drive from a real TTY as from a StringIO in tests.
|
||||||
|
def console(args, input: $stdin, out: $stdout)
|
||||||
|
options = parse_console_options(args)
|
||||||
|
options[:require].each { |path| require File.expand_path(path, @root) }
|
||||||
|
|
||||||
|
session = Object.new
|
||||||
|
session_binding = session.instance_eval { binding }
|
||||||
|
|
||||||
|
prompt = options[:prompt]
|
||||||
|
out.print(prompt)
|
||||||
|
input.each_line do |line|
|
||||||
|
line = line.chomp
|
||||||
|
stripped = line.strip
|
||||||
|
if %w[exit quit].include?(stripped)
|
||||||
|
break
|
||||||
|
elsif stripped.empty?
|
||||||
|
out.print(prompt)
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
result = session_binding.eval(line, '(console)')
|
||||||
|
out.puts "=> #{result.inspect}"
|
||||||
|
rescue StandardError, ScriptError => e
|
||||||
|
out.puts "Error: #{e.class}: #{e.message}"
|
||||||
|
end
|
||||||
|
out.print(prompt)
|
||||||
|
end
|
||||||
|
out.puts
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Runs Assets::Compiler directly - fingerprints every file under
|
||||||
|
# --source into --output plus a manifest.json - without booting the
|
||||||
|
# rest of the application (no Orm.connect, no migrations, no server).
|
||||||
|
# This is the missing deploy-time step Application#enable_assets never
|
||||||
|
# covered on its own: enable_assets compiles as a side effect of
|
||||||
|
# loading app.rb, which also means the ONLY way to precompile assets
|
||||||
|
# before this was to boot the entire app (database connection and all)
|
||||||
|
# just to get the compiled files onto disk. Mirrors `rake
|
||||||
|
# assets:precompile` in Rails: a standalone step meant to run once
|
||||||
|
# during deploy, before the server process starts.
|
||||||
|
def assets_precompile(args, out: $stdout)
|
||||||
|
options = parse_assets_precompile_options(args)
|
||||||
|
source = File.expand_path(options[:source], @root)
|
||||||
|
output = File.expand_path(options[:output], @root)
|
||||||
|
manifest = Assets::Compiler.new(source: source, output: output).compile!
|
||||||
|
out.puts "[prism assets:precompile] compiled #{manifest.to_h.size} file(s) into #{output}"
|
||||||
|
manifest
|
||||||
|
end
|
||||||
|
|
||||||
|
# Runs a Jobs::Worker as a long-lived daemon: polls the queue, drains
|
||||||
|
# every ready job, sleeps `interval` seconds, repeats until SIGINT/SIGTERM
|
||||||
|
# (or forever with --once/--max-cycles for scripted/finite runs). Returns
|
||||||
|
# the number of poll cycles executed, mainly so callers/tests can assert
|
||||||
|
# on it without needing a real infinite loop.
|
||||||
|
def jobs_work(args, out: $stdout)
|
||||||
|
options = parse_jobs_work_options(args)
|
||||||
|
options[:require].each { |path| require File.expand_path(path, @root) }
|
||||||
|
|
||||||
|
pidfile = options[:pidfile] && File.expand_path(options[:pidfile], @root)
|
||||||
|
write_pidfile(pidfile) if pidfile
|
||||||
|
|
||||||
|
queue = Jobs::Queue.new(File.expand_path(options[:file], @root))
|
||||||
|
worker = Jobs::Worker.new(
|
||||||
|
queue,
|
||||||
|
queue_name: options[:queue_name],
|
||||||
|
end
|
||||||
|
|
||||||
def new_app(name)
|
def new_app(name)
|
||||||
app_dir = File.join(@root, name)
|
app_dir = File.join(@root, name)
|
||||||
raise UsageError, "#{app_dir} already exists" if Dir.exist?(app_dir)
|
raise UsageError, "#{app_dir} already exists" if Dir.exist?(app_dir)
|
||||||
|
|||||||
Reference in new issue
Block a user