mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-16 22:12:53 +00:00
Condenses Documentation
This commit is contained in:
@@ -1,684 +1,124 @@
|
||||
# TurboVault - Development Guide
|
||||
# Development Guide
|
||||
|
||||
## Quick Start
|
||||
## Prerequisites
|
||||
|
||||
- Ruby 3.3+
|
||||
- Docker
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Start PostgreSQL
|
||||
# Clone repository
|
||||
git clone https://github.com/ryankazokas/turbovault-app.git
|
||||
cd turbovault
|
||||
|
||||
# Start services (PostgreSQL + Mailpit)
|
||||
task docker:up
|
||||
|
||||
# Setup database (first time only)
|
||||
# Setup database
|
||||
task db:setup
|
||||
|
||||
# Start Rails server
|
||||
task server
|
||||
|
||||
# Or use bin/dev for Tailwind watch mode
|
||||
bin/dev
|
||||
# Run development server
|
||||
task dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000 and create an account!
|
||||
Visit http://localhost:3000
|
||||
|
||||
## Development Workflow
|
||||
**Demo:** `demo@turbovault.com` / `password123`
|
||||
|
||||
### Daily Development
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
task docker:up # PostgreSQL
|
||||
bin/dev # Rails server + Tailwind watcher
|
||||
# Development
|
||||
task dev # Start Rails + CSS watcher
|
||||
task server # Rails only
|
||||
task console # Rails console
|
||||
|
||||
# Database
|
||||
task db:migrate # Run migrations
|
||||
task db:rollback # Rollback migration
|
||||
task db:reset # Reset database
|
||||
task db:seed # Load seed data
|
||||
|
||||
# Testing
|
||||
task test # Run tests
|
||||
task test:system # System tests
|
||||
|
||||
# Code Quality
|
||||
task lint # Check style
|
||||
task lint:fix # Auto-fix style
|
||||
task typecheck # Type checker
|
||||
task security # Security scan
|
||||
|
||||
# Type Checking
|
||||
task tapioca:init # First time only
|
||||
task tapioca:all # After bundle install
|
||||
task typecheck:watch # Watch mode
|
||||
|
||||
# Services
|
||||
task docker:up # Start services
|
||||
task docker:down # Stop services
|
||||
task docker:logs # View logs
|
||||
|
||||
# IGDB
|
||||
task igdb:sync # Manual sync
|
||||
task igdb:status # Check status
|
||||
task igdb:clear # Clear stuck jobs
|
||||
|
||||
# Cleanup
|
||||
task clean # Remove tmp/logs
|
||||
```
|
||||
|
||||
### 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
|
||||
├── controllers/ # Controllers
|
||||
├── models/ # Models
|
||||
├── views/ # ERB templates
|
||||
├── javascript/ # Stimulus controllers
|
||||
├── services/ # Service objects (IgdbService)
|
||||
└── jobs/ # Background jobs (IgdbSyncJob)
|
||||
|
||||
config/
|
||||
├── routes.rb # URL routing
|
||||
├── database.yml # Database configuration
|
||||
└── environments/ # Environment-specific config
|
||||
├── routes.rb # Routes
|
||||
├── database.yml # Database config
|
||||
├── queue.yml # Solid Queue config
|
||||
└── recurring.yml # Recurring jobs (IGDB sync)
|
||||
|
||||
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)
|
||||
k8s/ # Kubernetes manifests
|
||||
docs/ # Documentation
|
||||
```
|
||||
|
||||
## 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:
|
||||
Copy `.env.example` to `.env`:
|
||||
|
||||
```bash
|
||||
# First time setup
|
||||
kamal setup
|
||||
|
||||
# Deploy
|
||||
kamal deploy
|
||||
|
||||
# Check status
|
||||
kamal app exec --interactive --reuse "bin/rails console"
|
||||
# Optional: IGDB API
|
||||
IGDB_CLIENT_ID=your_client_id
|
||||
IGDB_CLIENT_SECRET=your_client_secret
|
||||
```
|
||||
|
||||
### Railway/Render
|
||||
Get credentials: https://dev.twitch.tv
|
||||
|
||||
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`
|
||||
## Contributing
|
||||
|
||||
1. Fork repository
|
||||
2. Create feature branch
|
||||
3. Make changes
|
||||
4. Run: `task test && task lint:fix && task typecheck`
|
||||
5. Submit PR
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database Connection Errors
|
||||
|
||||
```bash
|
||||
# Check if PostgreSQL is running
|
||||
docker compose ps
|
||||
# Database issues
|
||||
task docker:up && task db:reset
|
||||
|
||||
# Start PostgreSQL
|
||||
task docker:up
|
||||
# Type errors
|
||||
task tapioca:all && task typecheck
|
||||
|
||||
# Check database configuration
|
||||
cat config/database.yml
|
||||
# Tests failing
|
||||
task db:test:prepare && task test
|
||||
```
|
||||
|
||||
### 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! 🚀
|
||||
|
||||
Reference in New Issue
Block a user