mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-16 22:12:53 +00:00
685 lines
13 KiB
Markdown
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! 🚀
|