Merge pull request #51 from rede5/codex/refactor-backend-and-frontend-codebase

Codex-generated pull request
This commit is contained in:
Tiago Yamamoto 2026-02-14 15:42:57 -03:00 committed by GitHub
commit f4719a1a7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 188 additions and 34 deletions

135
backend/internal/database/database.go Executable file → Normal file
View file

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath"
"sort"
"strings" "strings"
"time" "time"
@ -49,48 +51,123 @@ func BuildConnectionString() (string, error) {
} }
func RunMigrations() { func RunMigrations() {
files, err := os.ReadDir("migrations") migrationDir, err := resolveMigrationDir()
if err != nil {
// Try fallback to relative path if running from cmd/api
files, err = os.ReadDir("../../migrations")
if err != nil { if err != nil {
log.Printf("⚠️ Warning: Could not list migrations directory: %v", err) log.Printf("⚠️ Warning: Could not list migrations directory: %v", err)
return return
} }
if err := ensureMigrationsTable(); err != nil {
log.Fatalf("❌ Error ensuring migrations table: %v", err)
} }
// Sort files by name to ensure order (001, 002, ...) entries, err := os.ReadDir(migrationDir)
// ReadDir returns sorted by name automatically, but let's be safe if logic changes? if err != nil {
// Actually ReadDir result is sorted by filename. log.Fatalf("❌ Error reading migrations directory: %v", err)
}
var files []os.DirEntry
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
continue
}
files = append(files, entry)
}
sort.Slice(files, func(i, j int) bool {
return files[i].Name() < files[j].Name()
})
warnDuplicateMigrationPrefixes(files)
for _, file := range files { for _, file := range files {
if file.IsDir() { name := file.Name()
if isMigrationApplied(name) {
log.Printf("⏭️ Migration %s skipped (already tracked)", name)
continue continue
} }
log.Printf("📦 Running migration: %s", file.Name()) path := filepath.Join(migrationDir, name)
content, err := os.ReadFile("migrations/" + file.Name()) content, err := os.ReadFile(path)
if err != nil { if err != nil {
// Try fallback log.Fatalf("❌ Error reading migration file %s: %v", name, err)
content, err = os.ReadFile("../../migrations/" + file.Name()) }
if err != nil {
log.Fatalf("❌ Error reading migration file %s: %v", file.Name(), err) log.Printf("📦 Running migration: %s", name)
if err := executeMigration(name, string(content)); err != nil {
log.Fatalf("❌ Error running migration %s: %v", name, err)
}
log.Printf("✅ Migration %s executed successfully", name)
}
log.Println("All migrations processed")
}
func resolveMigrationDir() (string, error) {
candidateDirs := []string{"migrations", "../../migrations"}
for _, dir := range candidateDirs {
if _, err := os.ReadDir(dir); err == nil {
return dir, nil
} }
} }
_, err = DB.Exec(string(content)) return "", fmt.Errorf("migrations directory not found in any known location")
if err != nil { }
errStr := err.Error()
// Check if it's an "already exists" error - these are safe to skip func ensureMigrationsTable() error {
if strings.Contains(errStr, "already exists") { _, err := DB.Exec(`
log.Printf("⏭️ Migration %s skipped (already applied)", file.Name()) CREATE TABLE IF NOT EXISTS schema_migrations (
} else { filename TEXT PRIMARY KEY,
// Real error - log it applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
log.Printf("❌ Error running migration %s: %v", file.Name(), err) )
} `)
} else { return err
log.Printf("✅ Migration %s executed successfully", file.Name()) }
}
} func isMigrationApplied(filename string) bool {
log.Println("All migrations processed") var exists bool
err := DB.QueryRow("SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE filename = $1)", filename).Scan(&exists)
if err != nil {
log.Fatalf("❌ Error checking migration %s: %v", filename, err)
}
return exists
}
func executeMigration(filename, sqlContent string) error {
tx, err := DB.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(sqlContent); err != nil {
errStr := strings.ToLower(err.Error())
if !strings.Contains(errStr, "already exists") {
return err
}
log.Printf("⏭️ Migration %s skipped due to existing resources", filename)
}
if _, err := tx.Exec("INSERT INTO schema_migrations (filename) VALUES ($1)", filename); err != nil {
return err
}
return tx.Commit()
}
func warnDuplicateMigrationPrefixes(files []os.DirEntry) {
prefixCount := make(map[string]int)
for _, file := range files {
parts := strings.SplitN(file.Name(), "_", 2)
if len(parts) == 2 {
prefixCount[parts[0]]++
}
}
for prefix, count := range prefixCount {
if count > 1 {
log.Printf("⚠️ Duplicate migration prefix detected (%s appears %d times)", prefix, count)
}
}
} }

View file

@ -60,8 +60,22 @@ func TestRunMigrations(t *testing.T) {
t.Fatalf("Failed to write migration file: %v", err) t.Fatalf("Failed to write migration file: %v", err)
} }
// Mock Expectation // Mock Expectations
mock.ExpectExec(regexp.QuoteMeta(`
CREATE TABLE IF NOT EXISTS schema_migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`)).WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectQuery(regexp.QuoteMeta("SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE filename = $1)")).
WithArgs("001_test.sql").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta(content)).WillReturnResult(sqlmock.NewResult(0, 0)) mock.ExpectExec(regexp.QuoteMeta(content)).WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO schema_migrations (filename) VALUES ($1)")).
WithArgs("001_test.sql").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// Run // Run
database.RunMigrations() database.RunMigrations()

View file

@ -13,12 +13,13 @@
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
}, },
"dependencies": { "dependencies": {
"@fastify/compress": "^8.0.1", "@fastify/compress": "^8.0.1",

View file

@ -64,6 +64,7 @@
"zustand": "^4.5.7" "zustand": "^4.5.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@playwright/test": "^1.57.0", "@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4.1.9", "@tailwindcss/postcss": "^4.1.9",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
@ -5087,6 +5088,66 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.7.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",

View file

@ -66,6 +66,7 @@
"zustand": "^4.5.7" "zustand": "^4.5.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@playwright/test": "^1.57.0", "@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4.1.9", "@tailwindcss/postcss": "^4.1.9",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",