con un clic
new-app
// Scaffold a new Rails app with event sourcing, following project conventions (todomvc as reference)
// Scaffold a new Rails app with event sourcing, following project conventions (todomvc as reference)
Plan a new feature end-to-end — impact analysis across all layers before delegating to /domain, /read-model, /controller skills
Create atomic git commits following project conventions
Build a new read model following project conventions (event handlers, tests, migration, configuration)
Upgrade Rails framework to a newer version following the smooth upgrade methodology
Upgrade RailsEventStore (RES) gems to a newer version
Create a new domain bounded context with aggregates, commands, events, and handlers
| name | new-app |
| description | Scaffold a new Rails app with event sourcing, following project conventions (todomvc as reference) |
Use this skill when asked to create a new Rails application in the apps/ directory. Each app is a standalone Rails application that uses domain modules from domains/ and the shared infra gem.
The apps/todo_mvc app is the canonical reference. All patterns below are extracted from it.
Before writing any code, clarify:
crm, inventory_tracker)domains/ or new ones to be created)Run from the apps/ directory. The rails alias may not work — use the full rbenv path:
cd apps && /Users/andrzej/.rbenv/versions/3.4.6/bin/rails new {app_name} --database=postgresql --css=tailwind --skip-test-unit
Important post-generation steps:
rails new creates a nested .git directory inside the new app. Remove it so the app is part of the parent repo: ask the user to run rm -rf apps/{app_name}/.gitcapybara and selenium-webdriver — replace the entire test group with mutant-minitest (see step 3)Add these gems to the generated Gemfile (after the last gem before the comments):
gem "rails_event_store", ">= 2.15.0", "< 3.0"
gem "arkency-command_bus"
gem "infra", path: "../../infra"
Replace the generated test group (which has capybara/selenium) with:
group :test do
gem "mutant-minitest"
end
Run bundle install.
Create config/initializers/rails_event_store.rb:
require "rails_event_store"
require "arkency/command_bus"
require_relative "../../lib/configuration"
Rails.configuration.to_prepare do
Rails.configuration.event_store = Infra::EventStore.main
Rails.configuration.command_bus = Arkency::CommandBus.new
Configuration.new.call(Rails.configuration.event_store, Rails.configuration.command_bus)
end
Create lib/configuration.rb:
require_relative "../../../domains/{domain_name}/lib/{domain_name}"
require_relative "../../../infra/lib/infra"
class Configuration
def call(event_store, command_bus)
enable_res_infra_event_linking(event_store)
# enable_{read_model_name}_read_model(event_store) — add as read models are created
{DomainModule}::Configuration.new.call(event_store, command_bus)
end
private
def enable_res_infra_event_linking(event_store)
[
RailsEventStore::LinkByEventType.new,
RailsEventStore::LinkByCorrelationId.new,
RailsEventStore::LinkByCausationId.new
].each { |h| event_store.subscribe_to_all_events(h) }
end
end
Each read model gets its own enable_* private method, called from call.
Generate the RES migration:
cd apps/{app_name} && rails generate rails_event_store_active_record:migration
Run rails db:create && rails db:migrate.
Ensure app/controllers/application_controller.rb has:
class ApplicationController < ActionController::Base
allow_browser versions: :modern
def command_bus
Rails.configuration.command_bus
end
def event_store
Rails.configuration.event_store
end
end
Replace the generated test/test_helper.rb (which has parallelize and fixtures :all):
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "mutant/minitest/coverage"
ActiveJob::Base.logger = Logger.new(nil)
class InMemoryRESTestCase < ActiveSupport::TestCase
def before_setup
result = super
@previous_event_store = Rails.configuration.event_store
@previous_command_bus = Rails.configuration.command_bus
Rails.configuration.event_store = Infra::EventStore.in_memory
Rails.configuration.command_bus = Arkency::CommandBus.new
Configuration.new.call(
Rails.configuration.event_store, Rails.configuration.command_bus
)
result
end
def before_teardown
result = super
Rails.configuration.event_store = @previous_event_store
Rails.configuration.command_bus = @previous_command_bus
result
end
def event_store
Rails.configuration.event_store
end
def command_bus
Rails.configuration.command_bus
end
end
class InMemoryRESIntegrationTestCase < ActionDispatch::IntegrationTest
def before_setup
result = super
@previous_event_store = Rails.configuration.event_store
@previous_command_bus = Rails.configuration.command_bus
Rails.configuration.event_store = Infra::EventStore.in_memory_rails
Rails.configuration.command_bus = Arkency::CommandBus.new
Configuration.new.call(Rails.configuration.event_store, Rails.configuration.command_bus)
result
end
def before_teardown
result = super
Rails.configuration.event_store = @previous_event_store
Rails.configuration.command_bus = @previous_command_bus
result
end
def command_bus
Rails.configuration.command_bus
end
end
Create .mutant.yml in the app root:
includes:
- test
requires:
- ./config/environment
integration: minitest
usage: opensource
coverage_criteria:
timeout: true
process_abort: true
matcher:
subjects:
- ReadModelName*
ignore:
- ReadModelName::ModelClass
- ReadModelName::Configuration#call
Add each read model's namespace to subjects and its AR model + Configuration#call to ignore.
Add targets to the root Makefile:
install-{app_name}:
@make -C apps/{app_name} install
test-{app_name}:
@make -C apps/{app_name} test
mutate-{app_name}:
@make -C apps/{app_name} mutate
Add install-{app_name} to the install: target, test-{app_name} to test:, and mutate-{app_name} to mutate:.
Create Makefile in the app directory:
install:
@bin/setup
dev:
@make -j 2 web css
test:
@bin/rails test
mutate:
@bundle exec mutant run
tailwind:
@bin/rails tailwindcss:build
css:
@bin/rails tailwindcss:watch
web:
@bin/rails server -p {port}
.PHONY: install dev test mutate tailwind css web
Use a unique port per app (3000 for rails_application, 3001+ for new apps).
Controllers dispatch commands and query read models:
class ResourceController < ApplicationController
def index
@records = ReadModelName.all
end
def create
id = SecureRandom.uuid
ActiveRecord::Base.transaction do
command_bus.call(DomainModule::CreateCommand.new(id: id, ...))
end
redirect_to root_path
end
def update
command_bus.call(DomainModule::UpdateCommand.new(id: params[:id], ...))
redirect_to root_path
end
def destroy
command_bus.call(DomainModule::DeleteCommand.new(id: params[:id]))
redirect_to root_path
end
end
Build the app incrementally, with tests passing at each step:
/domain skill)/read-model skill)make test and mutation testingrails command: The shell alias may not resolve. Use full path: /Users/andrzej/.rbenv/versions/3.4.6/bin/rails new.git: rails new creates its own git repo. Must be removed before committing to parent repobin/rails commands (migrations, generators): Must run from apps/{app_name}/ directory, not project rootapps/require_relative "../../../domains/{name}/lib/{name}"gem "infra", path: "../../infra"app/read_models/{name}/configuration.rbevent.data.fetch(:key)command_bus.call(...) for writes, read models for queriesSecureRandom.uuid)