Add models package with embedded DB interface pattern

- internal/models/model.go: DB interface + Base struct for all models
- internal/models/channel.go: Channel model with DB access for relation queries
- Database.NewChannel() factory injects db reference into model instances
- Uses interface to avoid circular imports (models -> db)
This commit is contained in:
clawbot
2026-02-09 12:24:23 -08:00
parent 8bb083a7f8
commit 5e9be8ccaf
3 changed files with 69 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
"embed"
"fmt"
"time"
"io/fs"
"log/slog"
"sort"
@@ -13,6 +14,7 @@ import (
"git.eeqj.de/sneak/chat/internal/config"
"git.eeqj.de/sneak/chat/internal/logger"
"git.eeqj.de/sneak/chat/internal/models"
"go.uber.org/fx"
_ "github.com/joho/godotenv/autoload"
@@ -29,11 +31,30 @@ type DatabaseParams struct {
}
type Database struct {
DB *sql.DB
db *sql.DB
log *slog.Logger
params *DatabaseParams
}
// GetDB implements models.db so Database can be embedded in model structs.
func (s *Database) GetDB() *sql.DB {
return s.db
}
// NewChannel creates a Channel model instance with the db reference injected.
func (s *Database) NewChannel(id int64, name, topic, modes string, createdAt, updatedAt time.Time) *models.Channel {
c := &models.Channel{
ID: id,
Name: name,
Topic: topic,
Modes: modes,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
c.SetDB(s)
return c
}
func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
s := new(Database)
s.params = &params
@@ -48,8 +69,8 @@ func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
},
OnStop: func(ctx context.Context) error {
s.log.Info("Database OnStop Hook")
if s.DB != nil {
return s.DB.Close()
if s.db != nil {
return s.db.Close()
}
return nil
},
@@ -76,7 +97,7 @@ func (s *Database) connect(ctx context.Context) error {
return err
}
s.DB = d
s.db = d
s.log.Info("database connected")
return s.runMigrations(ctx)
@@ -91,7 +112,7 @@ type migration struct {
func (s *Database) runMigrations(ctx context.Context) error {
// Bootstrap: create schema_migrations table directly (migration 001 also does this,
// but we need it to exist before we can check which migrations have run)
_, err := s.DB.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
_, err := s.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
@@ -139,7 +160,7 @@ func (s *Database) runMigrations(ctx context.Context) error {
for _, m := range migrations {
var exists int
err := s.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", m.version).Scan(&exists)
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", m.version).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check migration %d: %w", m.version, err)
}
@@ -148,10 +169,10 @@ func (s *Database) runMigrations(ctx context.Context) error {
}
s.log.Info("applying migration", "version", m.version, "name", m.name)
if _, err := s.DB.ExecContext(ctx, m.sql); err != nil {
if _, err := s.db.ExecContext(ctx, m.sql); err != nil {
return fmt.Errorf("failed to apply migration %d (%s): %w", m.version, m.name, err)
}
if _, err := s.DB.ExecContext(ctx, "INSERT INTO schema_migrations (version) VALUES (?)", m.version); err != nil {
if _, err := s.db.ExecContext(ctx, "INSERT INTO schema_migrations (version) VALUES (?)", m.version); err != nil {
return fmt.Errorf("failed to record migration %d: %w", m.version, err)
}
}
@@ -159,3 +180,4 @@ func (s *Database) runMigrations(ctx context.Context) error {
s.log.Info("database migrations complete")
return nil
}