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

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

@ -5,6 +5,8 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
@ -49,48 +51,123 @@ func BuildConnectionString() (string, error) {
}
func RunMigrations() {
files, err := os.ReadDir("migrations")
if err != nil {
// Try fallback to relative path if running from cmd/api
files, err = os.ReadDir("../../migrations")
migrationDir, err := resolveMigrationDir()
if err != nil {
log.Printf("⚠️ Warning: Could not list migrations directory: %v", err)
return
}
if err := ensureMigrationsTable(); err != nil {
log.Fatalf("❌ Error ensuring migrations table: %v", err)
}
// Sort files by name to ensure order (001, 002, ...)
// ReadDir returns sorted by name automatically, but let's be safe if logic changes?
// Actually ReadDir result is sorted by filename.
entries, err := os.ReadDir(migrationDir)
if err != nil {
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 {
if file.IsDir() {
name := file.Name()
if isMigrationApplied(name) {
log.Printf("⏭️ Migration %s skipped (already tracked)", name)
continue
}
log.Printf("📦 Running migration: %s", file.Name())
content, err := os.ReadFile("migrations/" + file.Name())
path := filepath.Join(migrationDir, name)
content, err := os.ReadFile(path)
if err != nil {
// Try fallback
content, err = os.ReadFile("../../migrations/" + file.Name())
if err != nil {
log.Fatalf("❌ Error reading migration file %s: %v", file.Name(), err)
log.Fatalf("❌ Error reading migration file %s: %v", 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")
}
func ensureMigrationsTable() error {
_, err := DB.Exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`)
return err
}
func isMigrationApplied(filename string) bool {
var exists bool
err := DB.QueryRow("SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE filename = $1)", filename).Scan(&exists)
if err != nil {
errStr := err.Error()
// Check if it's an "already exists" error - these are safe to skip
if strings.Contains(errStr, "already exists") {
log.Printf("⏭️ Migration %s skipped (already applied)", file.Name())
} else {
// Real error - log it
log.Printf("❌ Error running migration %s: %v", file.Name(), err)
log.Fatalf("❌ Error checking migration %s: %v", filename, err)
}
} else {
log.Printf("✅ Migration %s executed successfully", file.Name())
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)
}
}
log.Println("All migrations processed")
}

View file

@ -60,8 +60,22 @@ func TestRunMigrations(t *testing.T) {
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("INSERT INTO schema_migrations (filename) VALUES ($1)")).
WithArgs("001_test.sql").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// Run
database.RunMigrations()

View file

@ -13,12 +13,13 @@
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"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": {
"@fastify/compress": "^8.0.1",

View file

@ -64,6 +64,7 @@
"zustand": "^4.5.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4.1.9",
"@testing-library/dom": "^10.4.1",
@ -5087,6 +5088,66 @@
"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": {
"version": "4.1.18",
"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"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4.1.9",
"@testing-library/dom": "^10.4.1",