feat: Update database models and DTOs to use bytea for ULIDWrapper and add JWT configuration to environment

This commit is contained in:
2025-03-11 23:11:49 +00:00
parent c08da6fc92
commit 9057adebdd
19 changed files with 315 additions and 327 deletions
+1 -5
View File
@@ -3,7 +3,6 @@ package models
import (
"fmt"
"math/rand"
"runtime/debug"
"time"
"github.com/oklog/ulid/v2"
@@ -11,7 +10,7 @@ import (
)
type EntityBase struct {
ID ULIDWrapper `gorm:"type:char(26);primaryKey"`
ID ULIDWrapper `gorm:"type:bytea;primaryKey"`
CreatedAt time.Time `gorm:"index"`
UpdatedAt time.Time `gorm:"index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
@@ -19,9 +18,6 @@ type EntityBase struct {
// BeforeCreate is called by GORM before creating a record
func (eb *EntityBase) BeforeCreate(tx *gorm.DB) error {
fmt.Println("BeforeCreate called")
stack := debug.Stack()
fmt.Println("foo's stack:", string(stack))
if eb.ID.Compare(ULIDWrapper{}) == 0 { // If ID is empty
// Generate a new ULID
entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
+6 -6
View File
@@ -10,8 +10,8 @@ import (
// Customer represents a customer in the system
type Customer struct {
EntityBase
Name string `gorm:"column:name"`
CompanyID int `gorm:"column:company_id"`
Name string `gorm:"column:name"`
CompanyID ULIDWrapper `gorm:"type:bytea;column:company_id"`
}
// TableName specifies the table name for GORM
@@ -22,14 +22,14 @@ func (Customer) TableName() string {
// CustomerCreate contains the fields for creating a new customer
type CustomerCreate struct {
Name string
CompanyID int
CompanyID ULIDWrapper
}
// CustomerUpdate contains the updatable fields of a customer
type CustomerUpdate struct {
ID ULIDWrapper `gorm:"-"` // Exclude from updates
Name *string `gorm:"column:name"`
CompanyID *int `gorm:"column:company_id"`
ID ULIDWrapper `gorm:"-"` // Exclude from updates
Name *string `gorm:"column:name"`
CompanyID *ULIDWrapper `gorm:"column:company_id"`
}
// GetCustomerByID finds a customer by its ID
+25
View File
@@ -141,6 +141,31 @@ func CloseDB() error {
return nil
}
func GetGormDB(dbConfig DatabaseConfig, dbName string) (*gorm.DB, error) {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode)
// 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: dbConfig.LogLevel, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
Colorful: true, // Enable color
},
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: gormLogger,
})
if err != nil {
return nil, fmt.Errorf("error connecting to the database: %w", err)
}
return db, 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)
+1 -1
View File
@@ -13,7 +13,7 @@ import (
type Project struct {
EntityBase
Name string `gorm:"column:name;not null"`
CustomerID ULIDWrapper `gorm:"column:customer_id;type:char(26);not null"`
CustomerID ULIDWrapper `gorm:"column:customer_id;type:bytea;not null"`
// Relationships (for Eager Loading)
Customer *Customer `gorm:"foreignKey:CustomerID"`
+3 -3
View File
@@ -12,9 +12,9 @@ import (
// TimeEntry represents a time entry in the system
type TimeEntry struct {
EntityBase
UserID ULIDWrapper `gorm:"column:user_id;type:char(26);not null;index"`
ProjectID ULIDWrapper `gorm:"column:project_id;type:char(26);not null;index"`
ActivityID ULIDWrapper `gorm:"column:activity_id;type:char(26);not null;index"`
UserID ULIDWrapper `gorm:"column:user_id;type:bytea;not null;index"`
ProjectID ULIDWrapper `gorm:"column:project_id;type:bytea;not null;index"`
ActivityID ULIDWrapper `gorm:"column:activity_id;type:bytea;not null;index"`
Start time.Time `gorm:"column:start;not null"`
End time.Time `gorm:"column:end;not null"`
Description string `gorm:"column:description"`
+31 -29
View File
@@ -10,14 +10,14 @@ import (
"gorm.io/gorm/clause"
)
// ULIDWrapper wraps ulid.ULID to allow method definitions
// ULIDWrapper wraps ulid.ULID to make it work nicely with GORM
type ULIDWrapper struct {
ulid.ULID
}
// Compare implements the same comparison method as ulid.ULID
func (u ULIDWrapper) Compare(other ULIDWrapper) int {
return u.ULID.Compare(other.ULID)
// NewULIDWrapper creates a new ULIDWrapper with a new ULID
func NewULIDWrapper() ULIDWrapper {
return ULIDWrapper{ULID: ulid.Make()}
}
// FromULID creates a ULIDWrapper from a ulid.ULID
@@ -25,42 +25,30 @@ func FromULID(id ulid.ULID) ULIDWrapper {
return ULIDWrapper{ULID: id}
}
// From String creates a ULIDWrapper from a string
// ULIDWrapperFromString creates a ULIDWrapper from a string
func ULIDWrapperFromString(id string) (ULIDWrapper, error) {
parsed, err := ulid.Parse(id)
if err != nil {
return ULIDWrapper{}, fmt.Errorf("failed to parse ULID string: %w", err)
}
return FromULID(parsed), nil
return ULIDWrapper{ULID: parsed}, nil
}
// ToULID converts a ULIDWrapper to a ulid.ULID
func (u ULIDWrapper) ToULID() ulid.ULID {
return u.ULID
}
// GormValue implements the gorm.Valuer interface for ULIDWrapper
func (u ULIDWrapper) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return clause.Expr{
SQL: "?",
Vars: []any{u.String()},
}
}
// Scan implements the Scanner interface for ULIDWrapper
// Scan implements the sql.Scanner interface for ULIDWrapper
func (u *ULIDWrapper) Scan(src any) error {
switch v := src.(type) {
case []byte:
// If it's exactly 16 bytes, it's the binary representation
if len(v) == 16 {
copy(u.ULID[:], v)
return nil
}
// Otherwise, try as string
return fmt.Errorf("cannot scan []byte of length %d into ULIDWrapper", len(v))
case string:
parsed, err := ulid.Parse(v)
if err != nil {
return fmt.Errorf("failed to parse ULID string: %w", err)
}
u.ULID = parsed
return nil
case []byte:
parsed, err := ulid.Parse(string(v))
if err != nil {
return fmt.Errorf("failed to parse ULID bytes: %w", err)
return fmt.Errorf("failed to parse ULID: %w", err)
}
u.ULID = parsed
return nil
@@ -70,6 +58,20 @@ func (u *ULIDWrapper) Scan(src any) error {
}
// Value implements the driver.Valuer interface for ULIDWrapper
// Returns the binary representation of the ULID for maximum efficiency
func (u ULIDWrapper) Value() (driver.Value, error) {
return u.String(), nil
return u.ULID.Bytes(), nil
}
// GormValue implements the gorm.Valuer interface for ULIDWrapper
func (u ULIDWrapper) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return clause.Expr{
SQL: "?",
Vars: []any{u.Bytes()},
}
}
// Compare implements comparison for ULIDWrapper
func (u ULIDWrapper) Compare(other ULIDWrapper) int {
return u.ULID.Compare(other.ULID)
}
+1 -1
View File
@@ -39,7 +39,7 @@ type User struct {
Salt string `gorm:"column:salt;not null;type:varchar(64)"` // Base64-encoded Salt
Hash string `gorm:"column:hash;not null;type:varchar(128)"` // Base64-encoded Hash
Role string `gorm:"column:role;not null;default:'user'"`
CompanyID ULIDWrapper `gorm:"column:company_id;type:char(26);not null;index"`
CompanyID ULIDWrapper `gorm:"column:company_id;type:bytea;not null;index"`
HourlyRate float64 `gorm:"column:hourly_rate;not null;default:0"`
// Relationship for Eager Loading