package models import ( "crypto/rand" "database/sql/driver" "fmt" "time" "github.com/oklog/ulid/v2" "gorm.io/gorm" "gorm.io/gorm/schema" ) // ULID is a custom type that wraps oklog/ulid/v2.ULID and implements // GORM's Scanner and Valuer interfaces for database operations type ULID struct { ulid.ULID } // NewULID creates a new ULID from a ulid.ULID func NewULID(u ulid.ULID) ULID { return ULID{ULID: u} } // ParseULID parses a string into a ULID func ParseULID(s string) (ULID, error) { u, err := ulid.Parse(s) if err != nil { return ULID{}, err } return ULID{ULID: u}, nil } // MustParseULID parses a string into a ULID and panics on error func MustParseULID(s string) ULID { u, err := ParseULID(s) if err != nil { panic(err) } return u } // GenerateULID creates a new ULID with the current timestamp func GenerateULID() ULID { return ULID{ULID: ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader)} } // GenerateULIDWithTime creates a new ULID with the specified timestamp func GenerateULIDWithTime(t time.Time) ULID { return ULID{ULID: ulid.MustNew(ulid.Timestamp(t), rand.Reader)} } // Scan implements the sql.Scanner interface for reading from database func (u *ULID) Scan(value interface{}) error { if value == nil { *u = ULID{} return nil } switch v := value.(type) { case string: parsed, err := ulid.Parse(v) if err != nil { return fmt.Errorf("cannot parse ULID from string: %w", err) } u.ULID = parsed return nil case []byte: parsed, err := ulid.Parse(string(v)) if err != nil { return fmt.Errorf("cannot parse ULID from bytes: %w", err) } u.ULID = parsed return nil default: return fmt.Errorf("cannot scan %T into ULID", value) } } // Value implements the driver.Valuer interface for writing to database func (u ULID) Value() (driver.Value, error) { if u.ULID == (ulid.ULID{}) { return nil, nil } return u.ULID.String(), nil } // GormDataType returns the data type for GORM func (ULID) GormDataType() string { return "char(26)" } // GormDBDataType returns the database-specific data type for GORM func (ULID) GormDBDataType(db *gorm.DB, field *schema.Field) string { switch db.Dialector.Name() { case "postgres": return "char(26)" case "mysql": return "char(26)" case "sqlite": return "text" default: return "char(26)" } } // MarshalJSON implements json.Marshaler func (u ULID) MarshalJSON() ([]byte, error) { return []byte(`"` + u.ULID.String() + `"`), nil } // UnmarshalJSON implements json.Unmarshaler func (u *ULID) UnmarshalJSON(data []byte) error { if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' { return fmt.Errorf("invalid JSON string for ULID") } str := string(data[1 : len(data)-1]) if str == "" { *u = ULID{} return nil } parsed, err := ulid.Parse(str) if err != nil { return fmt.Errorf("cannot parse ULID from JSON: %w", err) } u.ULID = parsed return nil } // String returns the string representation of the ULID func (u ULID) String() string { return u.ULID.String() } // IsZero returns true if the ULID is zero value func (u ULID) IsZero() bool { return u.ULID == ulid.ULID{} }