feat: Add database migration, seeding, and testing commands with Makefile integration

This commit is contained in:
Jean Jacques Avril 2025-03-11 09:10:35 +00:00
parent baf656c093
commit 78be762430
8 changed files with 919 additions and 19 deletions

77
backend/Makefile Normal file
View File

@ -0,0 +1,77 @@
# Time Tracker Backend Makefile
.PHONY: db-start db-stop db-test model-test run build clean migrate seed help
# Default target
.DEFAULT_GOAL := help
# Variables
BINARY_NAME=timetracker
DB_CONTAINER=timetracker_db
# Help target
help:
@echo "Time Tracker Backend Makefile"
@echo ""
@echo "Usage:"
@echo " make db-start - Start the PostgreSQL database container"
@echo " make db-stop - Stop the PostgreSQL database container"
@echo " make db-test - Test the database connection"
@echo " make model-test - Test the database models"
@echo " make run - Run the application"
@echo " make build - Build the application"
@echo " make clean - Remove build artifacts"
@echo " make migrate - Run database migrations"
@echo " make seed - Seed the database with initial data"
@echo " make help - Show this help message"
# Start the database
db-start:
@echo "Starting PostgreSQL database container..."
@cd .. && docker-compose up -d db
@echo "Database container started"
# Stop the database
db-stop:
@echo "Stopping PostgreSQL database container..."
@cd .. && docker-compose stop db
@echo "Database container stopped"
# Test the database connection
db-test:
@echo "Testing database connection..."
@go run cmd/dbtest/main.go
# Test the database models
model-test:
@echo "Testing database models..."
@go run cmd/modeltest/main.go
# Run the application
run:
@echo "Running the application..."
@go run cmd/api/main.go
# Build the application
build:
@echo "Building the application..."
@go build -o $(BINARY_NAME) cmd/api/main.go
@echo "Build complete: $(BINARY_NAME)"
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
@rm -f $(BINARY_NAME)
@echo "Clean complete"
# Run database migrations
migrate:
@echo "Running database migrations..."
@go run -mod=mod cmd/migrate/main.go
@echo "Migrations complete"
# Seed the database with initial data
seed:
@echo "Seeding the database..."
@go run -mod=mod cmd/seed/main.go
@echo "Seeding complete"

147
backend/README.md Normal file
View File

@ -0,0 +1,147 @@
# Time Tracker Backend
This is the backend service for the Time Tracker application, built with Go, Gin, and GORM.
## Database Setup
The application uses PostgreSQL as its database. The database connection is configured using GORM, a popular Go ORM library.
### Configuration
Database configuration is handled through the `models.DatabaseConfig` struct in `internal/models/db.go`. The application uses sensible defaults that can be overridden with environment variables:
- `DB_HOST`: Database host (default: "localhost")
- `DB_PORT`: Database port (default: 5432)
- `DB_USER`: Database user (default: "timetracker")
- `DB_PASSWORD`: Database password (default: "password")
- `DB_NAME`: Database name (default: "timetracker")
- `DB_SSLMODE`: SSL mode (default: "disable")
### Running with Docker
The easiest way to run the database is using Docker Compose:
```bash
# Start the database
docker-compose up -d db
# Check if the database is running
docker-compose ps
```
### Database Migrations
The application automatically migrates the database schema on startup using GORM's AutoMigrate feature. This creates all necessary tables based on the model definitions.
### Initial Data Seeding
The application seeds the database with initial data if it's empty. This includes:
- A default company
- An admin user with email "admin@example.com" and password "Admin@123456"
## Running the Application
### Using Make Commands
The project includes a Makefile with common commands:
```bash
# Start the database
make db-start
# Test the database connection
make db-test
# Run database migrations
make migrate
# Seed the database with initial data
make seed
# Test the database models
make model-test
# Run the application
make run
# Build the application
make build
# Show all available commands
make help
```
### Manual Commands
If you prefer not to use Make, you can run the commands directly:
```bash
# Start the database
cd /path/to/timetracker
docker-compose up -d db
# Test the database connection
cd backend
go run cmd/dbtest/main.go
# Run database migrations
cd backend
go run cmd/migrate/main.go
# Seed the database with initial data
cd backend
go run cmd/seed/main.go
# Run the backend application
cd backend
go run cmd/api/main.go
```
The API will be available at http://localhost:8080/api and the Swagger documentation at http://localhost:8080/swagger/index.html.
### Environment Variables
You can configure the database connection using environment variables:
```bash
# Example: Connect to a different database
DB_HOST=my-postgres-server DB_PORT=5432 DB_USER=myuser DB_PASSWORD=mypassword DB_NAME=mydb go run cmd/api/main.go
```
## Database Models
The application uses the following models:
- `User`: Represents a user in the system
- `Company`: Represents a company
- `Customer`: Represents a customer
- `Project`: Represents a project
- `Activity`: Represents an activity
- `TimeEntry`: Represents a time entry
Each model has corresponding CRUD operations and relationships defined in the `internal/models` directory.
## GORM Best Practices
The application follows these GORM best practices:
1. **Connection Pooling**: Configured with sensible defaults for maximum idle connections, maximum open connections, and connection lifetime.
2. **Migrations**: Uses GORM's AutoMigrate to automatically create and update database tables.
3. **Transactions**: Uses transactions for operations that require multiple database changes to ensure data consistency.
4. **Soft Deletes**: Uses GORM's soft delete feature to mark records as deleted without actually removing them from the database.
5. **Relationships**: Properly defines relationships between models using GORM's relationship features.
6. **Error Handling**: Properly handles database errors and returns appropriate error messages.
7. **Context Support**: Uses context for database operations to support timeouts and cancellation.
8. **Logging**: Configures GORM's logger for appropriate logging based on the environment.
9. **Graceful Shutdown**: Properly closes database connections when the application shuts down.
10. **Validation**: Implements validation for model fields before saving to the database.

View File

@ -1,9 +1,14 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
@ -12,6 +17,7 @@ import (
"github.com/timetracker/backend/internal/api/routes"
"github.com/timetracker/backend/internal/models"
_ "gorm.io/driver/postgres"
"gorm.io/gorm/logger"
// GORM IMPORTS MARKER
)
@ -35,20 +41,59 @@ func helloHandler(c *gin.Context) {
}
func main() {
// Configure database
dbConfig := models.DatabaseConfig{
Host: "localhost",
Port: 5432,
User: "timetracker",
Password: "password",
DBName: "timetracker",
SSLMode: "disable", // For development environment
// Get database configuration with sensible defaults
dbConfig := models.DefaultDatabaseConfig()
// Override with environment variables if provided
if host := os.Getenv("DB_HOST"); host != "" {
dbConfig.Host = host
}
if port := os.Getenv("DB_PORT"); port != "" {
var portInt int
if _, err := fmt.Sscanf(port, "%d", &portInt); err == nil && portInt > 0 {
dbConfig.Port = portInt
}
}
if user := os.Getenv("DB_USER"); user != "" {
dbConfig.User = user
}
if password := os.Getenv("DB_PASSWORD"); password != "" {
dbConfig.Password = password
}
if dbName := os.Getenv("DB_NAME"); dbName != "" {
dbConfig.DBName = dbName
}
if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" {
dbConfig.SSLMode = sslMode
}
// Set log level based on environment
if gin.Mode() == gin.ReleaseMode {
dbConfig.LogLevel = logger.Error // Only log errors in production
} else {
dbConfig.LogLevel = logger.Info // Log more in development
}
// Initialize database
if err := models.InitDB(dbConfig); err != nil {
log.Fatalf("Error initializing database: %v", err)
}
defer func() {
if err := models.CloseDB(); err != nil {
log.Printf("Error closing database connection: %v", err)
}
}()
// Migrate database schema
if err := models.MigrateDB(); err != nil {
log.Fatalf("Error migrating database: %v", err)
}
// Seed database with initial data if needed
ctx := context.Background()
if err := models.SeedDB(ctx); err != nil {
log.Fatalf("Error seeding database: %v", err)
}
// Create Gin router
r := gin.Default()
@ -62,7 +107,34 @@ func main() {
// Swagger documentation
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Start server
fmt.Println("Server listening on port 8080")
r.Run(":8080")
// Create a server with graceful shutdown
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
// Start server in a goroutine
go func() {
fmt.Println("Server listening on port 8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Error starting server: %v", err)
}
}()
// Wait for interrupt signal to gracefully shut down the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Create a deadline for server shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Shutdown the server
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited properly")
}

View File

@ -0,0 +1,84 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/timetracker/backend/internal/models"
)
func main() {
// Get database configuration with sensible defaults
dbConfig := models.DefaultDatabaseConfig()
// Initialize database
fmt.Println("Connecting to database...")
if err := models.InitDB(dbConfig); err != nil {
log.Fatalf("Error initializing database: %v", err)
}
defer func() {
if err := models.CloseDB(); err != nil {
log.Printf("Error closing database connection: %v", err)
}
}()
fmt.Println("✓ Database connection successful")
// Test a simple query
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Get the database engine
db := models.GetEngine(ctx)
// Test database connection with a simple query
var result int
err := db.Raw("SELECT 1").Scan(&result).Error
if err != nil {
log.Fatalf("Error executing test query: %v", err)
}
fmt.Println("✓ Test query executed successfully")
// Check if tables exist
fmt.Println("Checking database tables...")
var tables []string
err = db.Raw("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'").Scan(&tables).Error
if err != nil {
log.Fatalf("Error checking tables: %v", err)
}
if len(tables) == 0 {
fmt.Println("No tables found. You may need to run migrations.")
fmt.Println("Attempting to run migrations...")
// Run migrations
if err := models.MigrateDB(); err != nil {
log.Fatalf("Error migrating database: %v", err)
}
fmt.Println("✓ Migrations completed successfully")
} else {
fmt.Println("Found tables:")
for _, table := range tables {
fmt.Printf(" - %s\n", table)
}
}
// Count users
var userCount int64
err = db.Model(&models.User{}).Count(&userCount).Error
if err != nil {
log.Fatalf("Error counting users: %v", err)
}
fmt.Printf("✓ User count: %d\n", userCount)
// Count companies
var companyCount int64
err = db.Model(&models.Company{}).Count(&companyCount).Error
if err != nil {
log.Fatalf("Error counting companies: %v", err)
}
fmt.Printf("✓ Company count: %d\n", companyCount)
fmt.Println("\nDatabase test completed successfully!")
}

View File

@ -0,0 +1,72 @@
package main
import (
"fmt"
"log"
"os"
"github.com/timetracker/backend/internal/models"
"gorm.io/gorm/logger"
)
func main() {
// Parse command line flags
verbose := false
for _, arg := range os.Args[1:] {
if arg == "--verbose" || arg == "-v" {
verbose = true
}
}
if verbose {
fmt.Println("Running in verbose mode")
}
// Get database configuration with sensible defaults
dbConfig := models.DefaultDatabaseConfig()
// Override with environment variables if provided
if host := os.Getenv("DB_HOST"); host != "" {
dbConfig.Host = host
}
if port := os.Getenv("DB_PORT"); port != "" {
var portInt int
if _, err := fmt.Sscanf(port, "%d", &portInt); err == nil && portInt > 0 {
dbConfig.Port = portInt
}
}
if user := os.Getenv("DB_USER"); user != "" {
dbConfig.User = user
}
if password := os.Getenv("DB_PASSWORD"); password != "" {
dbConfig.Password = password
}
if dbName := os.Getenv("DB_NAME"); dbName != "" {
dbConfig.DBName = dbName
}
if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" {
dbConfig.SSLMode = sslMode
}
// Set log level
dbConfig.LogLevel = logger.Info
// Initialize database
fmt.Println("Connecting to database...")
if err := models.InitDB(dbConfig); err != nil {
log.Fatalf("Error initializing database: %v", err)
}
defer func() {
if err := models.CloseDB(); err != nil {
log.Printf("Error closing database connection: %v", err)
}
}()
fmt.Println("✓ Database connection successful")
// Run migrations
fmt.Println("Running database migrations...")
if err := models.MigrateDB(); err != nil {
log.Fatalf("Error migrating database: %v", err)
}
fmt.Println("✓ Database migrations completed successfully")
}

View File

@ -0,0 +1,207 @@
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/oklog/ulid/v2"
"github.com/timetracker/backend/internal/models"
)
func main() {
// Get database configuration with sensible defaults
dbConfig := models.DefaultDatabaseConfig()
// Initialize database
fmt.Println("Connecting to database...")
if err := models.InitDB(dbConfig); err != nil {
log.Fatalf("Error initializing database: %v", err)
}
defer func() {
if err := models.CloseDB(); err != nil {
log.Printf("Error closing database connection: %v", err)
}
}()
fmt.Println("✓ Database connection successful")
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Test Company model
fmt.Println("\n=== Testing Company Model ===")
testCompanyModel(ctx)
// Test User model
fmt.Println("\n=== Testing User Model ===")
testUserModel(ctx)
// Test relationships
fmt.Println("\n=== Testing Relationships ===")
testRelationships(ctx)
fmt.Println("\nModel tests completed successfully!")
}
func testCompanyModel(ctx context.Context) {
// Create a new company
companyCreate := models.CompanyCreate{
Name: "Test Company",
}
company, err := models.CreateCompany(ctx, companyCreate)
if err != nil {
log.Fatalf("Error creating company: %v", err)
}
fmt.Printf("✓ Created company: %s (ID: %s)\n", company.Name, company.ID)
// Get the company by ID
retrievedCompany, err := models.GetCompanyByID(ctx, company.ID)
if err != nil {
log.Fatalf("Error getting company: %v", err)
}
if retrievedCompany == nil {
log.Fatalf("Company not found")
}
fmt.Printf("✓ Retrieved company: %s\n", retrievedCompany.Name)
// Update the company
newName := "Updated Test Company"
companyUpdate := models.CompanyUpdate{
ID: company.ID,
Name: &newName,
}
updatedCompany, err := models.UpdateCompany(ctx, companyUpdate)
if err != nil {
log.Fatalf("Error updating company: %v", err)
}
fmt.Printf("✓ Updated company name to: %s\n", updatedCompany.Name)
// Get all companies
companies, err := models.GetAllCompanies(ctx)
if err != nil {
log.Fatalf("Error getting all companies: %v", err)
}
fmt.Printf("✓ Retrieved %d companies\n", len(companies))
}
func testUserModel(ctx context.Context) {
// Get a company to associate with the user
companies, err := models.GetAllCompanies(ctx)
if err != nil || len(companies) == 0 {
log.Fatalf("Error getting companies or no companies found: %v", err)
}
companyID := companies[0].ID
// Create a new user
userCreate := models.UserCreate{
Email: "test@example.com",
Password: "Test@123456",
Role: models.RoleUser,
CompanyID: companyID,
HourlyRate: 50.0,
}
user, err := models.CreateUser(ctx, userCreate)
if err != nil {
log.Fatalf("Error creating user: %v", err)
}
fmt.Printf("✓ Created user: %s (ID: %s)\n", user.Email, user.ID)
// Get the user by ID
retrievedUser, err := models.GetUserByID(ctx, user.ID)
if err != nil {
log.Fatalf("Error getting user: %v", err)
}
if retrievedUser == nil {
log.Fatalf("User not found")
}
fmt.Printf("✓ Retrieved user: %s\n", retrievedUser.Email)
// Get the user by email
emailUser, err := models.GetUserByEmail(ctx, user.Email)
if err != nil {
log.Fatalf("Error getting user by email: %v", err)
}
if emailUser == nil {
log.Fatalf("User not found by email")
}
fmt.Printf("✓ Retrieved user by email: %s\n", emailUser.Email)
// Update the user
newEmail := "updated@example.com"
newRole := models.RoleAdmin
newHourlyRate := 75.0
userUpdate := models.UserUpdate{
ID: user.ID,
Email: &newEmail,
Role: &newRole,
HourlyRate: &newHourlyRate,
}
updatedUser, err := models.UpdateUser(ctx, userUpdate)
if err != nil {
log.Fatalf("Error updating user: %v", err)
}
fmt.Printf("✓ Updated user email to: %s, role to: %s\n", updatedUser.Email, updatedUser.Role)
// Test authentication
authUser, err := models.AuthenticateUser(ctx, updatedUser.Email, "Test@123456")
if err != nil {
log.Fatalf("Error authenticating user: %v", err)
}
if authUser == nil {
log.Fatalf("Authentication failed")
}
fmt.Printf("✓ User authentication successful\n")
// Get all users
users, err := models.GetAllUsers(ctx)
if err != nil {
log.Fatalf("Error getting all users: %v", err)
}
fmt.Printf("✓ Retrieved %d users\n", len(users))
// Get users by company ID
companyUsers, err := models.GetUsersByCompanyID(ctx, companyID)
if err != nil {
log.Fatalf("Error getting users by company ID: %v", err)
}
fmt.Printf("✓ Retrieved %d users for company ID: %s\n", len(companyUsers), companyID)
}
func testRelationships(ctx context.Context) {
// Get a user with company
users, err := models.GetAllUsers(ctx)
if err != nil || len(users) == 0 {
log.Fatalf("Error getting users or no users found: %v", err)
}
userID := users[0].ID
// Get user with company
user, err := models.GetUserWithCompany(ctx, userID)
if err != nil {
log.Fatalf("Error getting user with company: %v", err)
}
if user == nil {
log.Fatalf("User not found")
}
if user.Company == nil {
log.Fatalf("User's company not loaded")
}
fmt.Printf("✓ Retrieved user %s with company %s\n", user.Email, user.Company.Name)
// Test invalid ID
invalidID := ulid.MustNew(ulid.Timestamp(time.Now()), ulid.DefaultEntropy())
invalidUser, err := models.GetUserByID(ctx, invalidID)
if err != nil {
log.Fatalf("Error getting user with invalid ID: %v", err)
}
if invalidUser != nil {
log.Fatalf("User found with invalid ID")
}
fmt.Printf("✓ Correctly handled invalid user ID\n")
}

89
backend/cmd/seed/main.go Normal file
View File

@ -0,0 +1,89 @@
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/timetracker/backend/internal/models"
"gorm.io/gorm/logger"
)
func main() {
// Parse command line flags
force := false
for _, arg := range os.Args[1:] {
if arg == "--force" || arg == "-f" {
force = true
}
}
// Get database configuration with sensible defaults
dbConfig := models.DefaultDatabaseConfig()
// Override with environment variables if provided
if host := os.Getenv("DB_HOST"); host != "" {
dbConfig.Host = host
}
if port := os.Getenv("DB_PORT"); port != "" {
var portInt int
if _, err := fmt.Sscanf(port, "%d", &portInt); err == nil && portInt > 0 {
dbConfig.Port = portInt
}
}
if user := os.Getenv("DB_USER"); user != "" {
dbConfig.User = user
}
if password := os.Getenv("DB_PASSWORD"); password != "" {
dbConfig.Password = password
}
if dbName := os.Getenv("DB_NAME"); dbName != "" {
dbConfig.DBName = dbName
}
if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" {
dbConfig.SSLMode = sslMode
}
// Set log level
dbConfig.LogLevel = logger.Info
// Initialize database
fmt.Println("Connecting to database...")
if err := models.InitDB(dbConfig); err != nil {
log.Fatalf("Error initializing database: %v", err)
}
defer func() {
if err := models.CloseDB(); err != nil {
log.Printf("Error closing database connection: %v", err)
}
}()
fmt.Println("✓ Database connection successful")
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Check if we need to seed (e.g., no companies exist)
if !force {
var count int64
db := models.GetEngine(ctx)
if err := db.Model(&models.Company{}).Count(&count).Error; err != nil {
log.Fatalf("Error checking if seeding is needed: %v", err)
}
// If data already exists, skip seeding
if count > 0 {
fmt.Println("Database already contains data. Use --force to override.")
return
}
}
// Seed the database
fmt.Println("Seeding database with initial data...")
if err := models.SeedDB(ctx); err != nil {
log.Fatalf("Error seeding database: %v", err)
}
fmt.Println("✓ Database seeding completed successfully")
}

View File

@ -4,11 +4,14 @@ import (
"context"
"errors"
"fmt"
"log"
"reflect"
"strings"
"time"
"gorm.io/driver/postgres" // For PostgreSQL
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Global variable for the DB connection
@ -16,12 +19,32 @@ var defaultDB *gorm.DB
// DatabaseConfig contains the configuration data for the database connection
type DatabaseConfig struct {
Host string
Port int
User string
Password string
DBName string
SSLMode string
Host string
Port int
User string
Password string
DBName string
SSLMode string
MaxIdleConns int // Maximum number of idle connections
MaxOpenConns int // Maximum number of open connections
MaxLifetime time.Duration // Maximum lifetime of a connection
LogLevel logger.LogLevel
}
// DefaultDatabaseConfig returns a default configuration with sensible values
func DefaultDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
Host: "localhost",
Port: 5432,
User: "timetracker",
Password: "password",
DBName: "timetracker",
SSLMode: "disable",
MaxIdleConns: 10,
MaxOpenConns: 100,
MaxLifetime: time.Hour,
LogLevel: logger.Info,
}
}
// InitDB initializes the database connection (once at startup)
@ -31,22 +54,151 @@ func InitDB(config DatabaseConfig) error {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
// Establish database connection
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// Configure GORM logger
gormLogger := logger.New(
log.New(log.Writer(), "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: 200 * time.Millisecond, // Slow SQL threshold
LogLevel: config.LogLevel, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
Colorful: true, // Enable color
},
)
// Establish database connection with custom logger
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: gormLogger,
})
if err != nil {
return fmt.Errorf("error connecting to the database: %w", err)
}
// Configure connection pool
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("error getting database connection: %w", err)
}
// Set connection pool parameters
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
sqlDB.SetConnMaxLifetime(config.MaxLifetime)
defaultDB = db
return nil
}
// MigrateDB performs database migrations for all models
func MigrateDB() error {
if defaultDB == nil {
return errors.New("database not initialized")
}
log.Println("Starting database migration...")
// Add all models that should be migrated here
err := defaultDB.AutoMigrate(
&Company{},
&User{},
&Customer{},
&Project{},
&Activity{},
&TimeEntry{},
)
if err != nil {
return fmt.Errorf("error migrating database: %w", err)
}
log.Println("Database migration completed successfully")
return nil
}
// SeedDB seeds the database with initial data if needed
func SeedDB(ctx context.Context) error {
if defaultDB == nil {
return errors.New("database not initialized")
}
log.Println("Checking if database seeding is needed...")
// Check if we need to seed (e.g., no companies exist)
var count int64
if err := defaultDB.Model(&Company{}).Count(&count).Error; err != nil {
return fmt.Errorf("error checking if seeding is needed: %w", err)
}
// If data already exists, skip seeding
if count > 0 {
log.Println("Database already contains data, skipping seeding")
return nil
}
log.Println("Seeding database with initial data...")
// Start a transaction for all seed operations
return defaultDB.Transaction(func(tx *gorm.DB) error {
// Create a default company
defaultCompany := Company{
Name: "Default Company",
}
if err := tx.Create(&defaultCompany).Error; err != nil {
return fmt.Errorf("error creating default company: %w", err)
}
// Create an admin user
adminUser := User{
Email: "admin@example.com",
Role: RoleAdmin,
CompanyID: defaultCompany.ID,
HourlyRate: 100.0,
}
// Hash a default password
pwData, err := HashPassword("Admin@123456")
if err != nil {
return fmt.Errorf("error hashing password: %w", err)
}
adminUser.Salt = pwData.Salt
adminUser.Hash = pwData.Hash
if err := tx.Create(&adminUser).Error; err != nil {
return fmt.Errorf("error creating admin user: %w", err)
}
log.Println("Database seeding completed successfully")
return nil
})
}
// GetEngine returns the DB instance, possibly with context
func GetEngine(ctx context.Context) *gorm.DB {
if defaultDB == nil {
panic("database not initialized")
}
// If a special transaction is in ctx, you could check it here
return defaultDB.WithContext(ctx)
}
// CloseDB closes the database connection
func CloseDB() error {
if defaultDB == nil {
return nil
}
sqlDB, err := defaultDB.DB()
if err != nil {
return fmt.Errorf("error getting database connection: %w", err)
}
if err := sqlDB.Close(); err != nil {
return fmt.Errorf("error closing database connection: %w", err)
}
return nil
}
// UpdateModel updates a model based on the set pointer fields
func UpdateModel(ctx context.Context, model any, updates any) error {
updateValue := reflect.ValueOf(updates)