Files
turbovault-app/docs/DEVELOPMENT_GUIDE.md
2026-03-29 02:37:49 -04:00

685 lines
13 KiB
Markdown

# TurboVault - Development Guide
## Quick Start
```bash
# Start PostgreSQL
task docker:up
# Setup database (first time only)
task db:setup
# Start Rails server
task server
# Or use bin/dev for Tailwind watch mode
bin/dev
```
Visit http://localhost:3000 and create an account!
## Development Workflow
### Daily Development
```bash
# Start all services
task docker:up # PostgreSQL
bin/dev # Rails server + Tailwind watcher
```
### Database Operations
```bash
# Create a new migration
rails generate migration AddFieldToModel field:type
# Run migrations
task db:migrate
# Rollback last migration
task db:rollback
# Reset database (drops, creates, migrates, seeds)
task db:reset
# Open Rails console
task console
# Check pending migrations
rails db:migrate:status
```
### Generating Code
```bash
# Generate a model
rails generate model ModelName field:type
# Generate a controller
rails generate controller ControllerName action1 action2
# Generate a scaffold (model + controller + views)
rails generate scaffold ModelName field:type
# Destroy generated code
rails destroy model ModelName
```
### Running Tests
```bash
# Run all tests
task test
# Run specific test file
rails test test/models/game_test.rb
# Run specific test
rails test test/models/game_test.rb:10
# Run system tests
task test:system
```
### Code Quality
```bash
# Run RuboCop linter
task lint
# Auto-fix RuboCop issues
task lint:fix
# Security checks
task security
```
### Type Checking with Sorbet
TurboVault uses Sorbet for gradual static type checking.
```bash
# First time setup (after bundle install)
task tapioca:init
task tapioca:all
# Run type checker
task typecheck
# Watch mode (re-checks on file changes)
task typecheck:watch
# Update type definitions after gem/model changes
task tapioca:gems # After bundle install
task tapioca:dsl # After model changes
```
**See [SORBET.md](SORBET.md) for complete type checking guide.**
## Project Structure
```
app/
├── controllers/
│ ├── concerns/ # Shared controller modules
│ ├── api/v1/ # API controllers
│ └── *.rb # Web controllers
├── models/
│ ├── concerns/ # Shared model modules
│ └── *.rb # ActiveRecord models
├── views/
│ ├── layouts/ # Application layouts
│ └── */ # Controller-specific views
├── helpers/ # View helpers
├── mailers/ # Email mailers
└── jobs/ # Background jobs
config/
├── routes.rb # URL routing
├── database.yml # Database configuration
└── environments/ # Environment-specific config
db/
├── migrate/ # Database migrations
├── seeds.rb # Seed data
└── schema.rb # Current database schema
test/
├── models/ # Model tests
├── controllers/ # Controller tests
├── system/ # End-to-end tests
└── fixtures/ # Test data
```
## Common Tasks
### Adding a New Model
1. Generate the model:
```bash
rails generate model Post user:references title:string body:text published:boolean
```
2. Edit the migration (add indexes, constraints, RLS if user-scoped):
```ruby
class CreatePosts < ActiveRecord::Migration[8.1]
def change
create_table :posts do |t|
t.references :user, null: false, foreign_key: true, index: true
t.string :title, null: false
t.text :body
t.boolean :published, default: false, null: false
t.timestamps
end
add_index :posts, :title
# Enable RLS if user-scoped
execute <<-SQL
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY posts_isolation_policy ON posts
USING (user_id = current_setting('app.current_user_id', true)::bigint);
SQL
end
end
```
3. Run the migration:
```bash
rails db:migrate
```
4. Add associations and validations to the model:
```ruby
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true
validates :body, presence: true
scope :published, -> { where(published: true) }
end
```
5. Add association to User model:
```ruby
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
```
### Adding a New Controller
1. Generate the controller:
```bash
rails generate controller Posts index show new create edit update destroy
```
2. Implement controller actions:
```ruby
class PostsController < ApplicationController
before_action :require_authentication
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = current_user.posts.order(created_at: :desc)
end
def show
end
def new
@post = current_user.posts.build
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: "Post created successfully."
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @post.update(post_params)
redirect_to @post, notice: "Post updated successfully."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_path, notice: "Post deleted successfully."
end
private
def set_post
@post = current_user.posts.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :published)
end
end
```
3. Add routes:
```ruby
resources :posts
```
4. Create views in `app/views/posts/`
### Adding an API Endpoint
1. Create API controller:
```bash
mkdir -p app/controllers/api/v1
```
2. Create controller file:
```ruby
# app/controllers/api/v1/posts_controller.rb
module Api
module V1
class PostsController < BaseController
def index
@posts = current_user.posts.order(created_at: :desc)
render json: @posts
end
def show
@post = current_user.posts.find(params[:id])
render json: @post
end
def create
@post = current_user.posts.build(post_params)
if @post.save
render json: @post, status: :created
else
render json: { errors: @post.errors.full_messages },
status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :body, :published)
end
end
end
end
```
3. Add API routes:
```ruby
namespace :api do
namespace :v1 do
resources :posts, only: [:index, :show, :create]
end
end
```
### Adding a Background Job
1. Generate the job:
```bash
rails generate job ProcessData
```
2. Implement the job:
```ruby
class ProcessDataJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
# Do some processing
end
end
```
3. Enqueue the job:
```ruby
ProcessDataJob.perform_later(user.id)
```
### Adding Email Functionality
1. Generate a mailer:
```bash
rails generate mailer UserMailer welcome
```
2. Implement the mailer:
```ruby
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
mail(to: @user.email, subject: "Welcome to TurboVault!")
end
end
```
3. Create email templates in `app/views/user_mailer/`
4. Configure SMTP in `config/environments/`:
```ruby
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SMTP_ADDRESS'],
port: ENV['SMTP_PORT'],
user_name: ENV['SMTP_USERNAME'],
password: ENV['SMTP_PASSWORD'],
authentication: 'plain',
enable_starttls_auto: true
}
```
5. Send the email:
```ruby
UserMailer.welcome(user).deliver_later
```
## Testing Guide
### Writing Model Tests
```ruby
# test/models/game_test.rb
require "test_helper"
class GameTest < ActiveSupport::TestCase
test "should not save game without title" do
game = Game.new
assert_not game.save, "Saved game without title"
end
test "should save valid game" do
game = games(:one) # Uses fixture
assert game.save, "Failed to save valid game"
end
test "should belong to user" do
game = games(:one)
assert_respond_to game, :user
end
end
```
### Writing Controller Tests
```ruby
# test/controllers/games_controller_test.rb
require "test_helper"
class GamesControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:one)
sign_in_as @user # Helper method to sign in
@game = games(:one)
end
test "should get index" do
get games_url
assert_response :success
end
test "should create game" do
assert_difference('Game.count') do
post games_url, params: {
game: {
title: "New Game",
platform_id: platforms(:one).id,
format: "physical"
}
}
end
assert_redirected_to game_path(Game.last)
end
end
```
### Writing System Tests
```ruby
# test/system/games_test.rb
require "application_system_test_case"
class GamesTest < ApplicationSystemTestCase
setup do
@user = users(:one)
sign_in_as @user
end
test "visiting the index" do
visit games_url
assert_selector "h1", text: "My Games"
end
test "creating a game" do
visit games_url
click_on "Add Game"
fill_in "Title", with: "Test Game"
select "Nintendo 64", from: "Platform"
select "Physical", from: "Format"
click_on "Create Game"
assert_text "Game was successfully created"
end
end
```
## Debugging
### Rails Console
```bash
rails console
# Test queries
User.first
Game.where(format: :physical).count
current_user.games.includes(:platform)
# Test RLS
ActiveRecord::Base.connection.execute(
"SET LOCAL app.current_user_id = 1"
)
```
### Logs
```bash
# Tail development log
tail -f log/development.log
# View specific log
cat log/test.log
```
### Debug with Byebug
Add to your code:
```ruby
require 'debug'
debugger # Execution will pause here
```
Then interact in the terminal:
```
n # Next line
c # Continue
p var # Print variable
exit # Exit debugger
```
## Performance Tips
### Avoid N+1 Queries
Bad:
```ruby
@games = current_user.games
# In view: @games.each { |g| g.platform.name } # N+1!
```
Good:
```ruby
@games = current_user.games.includes(:platform)
```
### Use Database Indexes
```ruby
add_index :games, :title
add_index :games, [:user_id, :platform_id]
```
### Use Counter Caches
```ruby
class Collection < ApplicationRecord
has_many :games, counter_cache: true
end
```
### Pagination
```ruby
@games = current_user.games.page(params[:page]).per(25)
```
## Environment Variables
Create `.env` for development (never commit!):
```
DATABASE_HOST=localhost
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
SMTP_ADDRESS=smtp.example.com
SMTP_USERNAME=user@example.com
SMTP_PASSWORD=secret
```
Load with:
```ruby
# config/application.rb
config.before_configuration do
env_file = Rails.root.join('.env')
if File.exist?(env_file)
File.readlines(env_file).each do |line|
key, value = line.split('=', 2)
ENV[key.strip] = value.strip if key && value
end
end
end
```
## Deployment
### Kamal (Recommended)
Already configured! Just:
```bash
# First time setup
kamal setup
# Deploy
kamal deploy
# Check status
kamal app exec --interactive --reuse "bin/rails console"
```
### Railway/Render
1. Push to Git
2. Connect repository
3. Set environment variables
4. Add build command: `bundle install && rails db:migrate`
5. Add start command: `rails server -b 0.0.0.0`
## Troubleshooting
### Database Connection Errors
```bash
# Check if PostgreSQL is running
docker compose ps
# Start PostgreSQL
task docker:up
# Check database configuration
cat config/database.yml
```
### Asset Issues
```bash
# Rebuild assets
rails assets:precompile
# Rebuild Tailwind
rails tailwindcss:build
```
### Migration Issues
```bash
# Check migration status
rails db:migrate:status
# Rollback and retry
rails db:rollback
rails db:migrate
```
## Resources
- [Rails Guides](https://guides.rubyonrails.org/)
- [Rails API Documentation](https://api.rubyonrails.org/)
- [Tailwind CSS Docs](https://tailwindcss.com/docs)
- [Hotwire Documentation](https://hotwired.dev/)
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
## Getting Help
1. Check the logs: `tail -f log/development.log`
2. Use Rails console: `rails console`
3. Check database: `rails dbconsole`
4. Read the error message carefully
5. Search Stack Overflow
6. Check Rails Guides
## Best Practices
1. **Always filter by current_user** in controllers
2. **Use strong parameters** for mass assignment
3. **Add validations** to models
4. **Write tests** for new features
5. **Keep controllers thin** - move logic to models
6. **Use concerns** for shared code
7. **Keep views simple** - use helpers for complex logic
8. **Add indexes** for frequently queried columns
9. **Use scopes** for common queries
10. **Document your API** endpoints
Happy coding! 🚀