Automated Action 887703b6a2 Add Go Todo App implementation
This commit adds a complete Go implementation of the Todo application. The
application uses Gin framework for the web server, GORM for database access,
and SQLite for storage.

Key features:
- Todo CRUD operations with the same API endpoints
- Health check endpoint
- Database migrations
- Tests for models, services, and API handlers
- Documentation for the API
- Configurable settings
2025-05-17 22:20:05 +00:00

152 lines
3.6 KiB
Go

package database
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/simpletodoapp/go-todo-app/internal/config"
)
// MigrationRunner handles database migrations
type MigrationRunner struct {
db *Database
config *config.Config
}
// NewMigrationRunner creates a new migration runner
func NewMigrationRunner(db *Database, cfg *config.Config) *MigrationRunner {
return &MigrationRunner{
db: db,
config: cfg,
}
}
// RunMigrations executes all pending migrations
func (m *MigrationRunner) RunMigrations(migrationsPath string) error {
log.Println("Running migrations...")
// Get all migration files
files, err := getMigrationFiles(migrationsPath)
if err != nil {
return err
}
if len(files) == 0 {
log.Println("No migration files found")
return nil
}
// Create migrations table if it doesn't exist
err = m.createMigrationsTable()
if err != nil {
return err
}
// Get applied migrations
appliedMigrations, err := m.getAppliedMigrations()
if err != nil {
return err
}
// Apply pending migrations
for _, file := range files {
migrationName := filepath.Base(file)
if !contains(appliedMigrations, migrationName) {
log.Printf("Applying migration: %s", migrationName)
// Read migration file
content, err := ioutil.ReadFile(file)
if err != nil {
return fmt.Errorf("failed to read migration file: %v", err)
}
// Split into up and down migrations
parts := strings.Split(string(content), "-- Down migration")
upMigration := parts[0]
// Execute migration
tx := m.db.DB.Begin()
if err := tx.Exec(upMigration).Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to apply migration: %v", err)
}
// Record applied migration
if err := tx.Exec("INSERT INTO migrations (name, applied_at) VALUES (?, CURRENT_TIMESTAMP)", migrationName).Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to record migration: %v", err)
}
tx.Commit()
log.Printf("Migration applied: %s", migrationName)
} else {
log.Printf("Migration already applied: %s", migrationName)
}
}
log.Println("Migrations completed successfully")
return nil
}
// createMigrationsTable creates the migrations table if it doesn't exist
func (m *MigrationRunner) createMigrationsTable() error {
query := `
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
applied_at DATETIME NOT NULL
)
`
return m.db.DB.Exec(query).Error
}
// getAppliedMigrations gets the list of already applied migrations
func (m *MigrationRunner) getAppliedMigrations() ([]string, error) {
var migrations []string
var records []struct {
Name string
}
result := m.db.DB.Raw("SELECT name FROM migrations ORDER BY id ASC").Scan(&records)
if result.Error != nil {
return nil, fmt.Errorf("failed to get applied migrations: %v", result.Error)
}
for _, record := range records {
migrations = append(migrations, record.Name)
}
return migrations, nil
}
// getMigrationFiles gets all migration files sorted by filename
func getMigrationFiles(migrationsPath string) ([]string, error) {
files, err := filepath.Glob(filepath.Join(migrationsPath, "*.sql"))
if err != nil {
return nil, fmt.Errorf("failed to read migration files: %v", err)
}
// Check if directory exists
if _, err := os.Stat(migrationsPath); os.IsNotExist(err) {
return []string{}, nil
}
sort.Strings(files)
return files, nil
}
// contains checks if a string is in a slice
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}