Merge pull request #51 from rede5/codex/refactor-backend-and-frontend-codebase
Codex-generated pull request
This commit is contained in:
commit
f4719a1a7a
5 changed files with 188 additions and 34 deletions
137
backend/internal/database/database.go
Executable file → Normal file
137
backend/internal/database/database.go
Executable file → Normal 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 {
|
if err != nil {
|
||||||
// Try fallback to relative path if running from cmd/api
|
log.Printf("⚠️ Warning: Could not list migrations directory: %v", err)
|
||||||
files, err = os.ReadDir("../../migrations")
|
return
|
||||||
if err != nil {
|
|
||||||
log.Printf("⚠️ Warning: Could not list migrations directory: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort files by name to ensure order (001, 002, ...)
|
if err := ensureMigrationsTable(); err != nil {
|
||||||
// ReadDir returns sorted by name automatically, but let's be safe if logic changes?
|
log.Fatalf("❌ Error ensuring migrations table: %v", err)
|
||||||
// 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 {
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = DB.Exec(string(content))
|
log.Printf("📦 Running migration: %s", name)
|
||||||
if err != nil {
|
if err := executeMigration(name, string(content)); err != nil {
|
||||||
errStr := err.Error()
|
log.Fatalf("❌ Error running migration %s: %v", name, err)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Printf("✅ Migration %s executed successfully", file.Name())
|
|
||||||
}
|
}
|
||||||
|
log.Printf("✅ Migration %s executed successfully", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("All migrations processed")
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -98,4 +99,4 @@
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
frontend/package-lock.json
generated
61
frontend/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue