135 lines
3.7 KiB
Go
135 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/glebarez/sqlite" // pure-Go SQLite (no CGO — keeps the static build)
|
|
"github.com/jkaninda/logger"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Entry is a single signature on the guestbook wall.
|
|
type Entry struct {
|
|
ID uint `gorm:"primaryKey" json:"id"`
|
|
Name string `gorm:"type:varchar(60);not null" json:"name"`
|
|
Message string `gorm:"type:varchar(500);not null" json:"message"`
|
|
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
|
UpdatedAt time.Time `json:"-"`
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
}
|
|
|
|
type Store struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewStore opens a GORM connection using the given driver ("postgres" or
|
|
// "sqlite") and DSN. It does not block on the database being reachable — call
|
|
// Ping (e.g. from the health check) for that.
|
|
func NewStore(driver, dsn string) (*Store, error) {
|
|
var dialector gorm.Dialector
|
|
switch driver {
|
|
case "sqlite", "sqlite3":
|
|
// Create the parent directory (e.g. ./data or /data) so the SQLite
|
|
// file can be written on first run.
|
|
if err := ensureSQLiteDir(dsn); err != nil {
|
|
return nil, fmt.Errorf("create sqlite directory: %w", err)
|
|
}
|
|
dialector = sqlite.Open(dsn)
|
|
case "postgres", "":
|
|
dialector = postgres.Open(dsn)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported DB_DRIVER %q (want postgres or sqlite)", driver)
|
|
}
|
|
|
|
db, err := gorm.Open(dialector, &gorm.Config{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logger.Info("database connected", "driver", driver)
|
|
return &Store{db: db}, nil
|
|
}
|
|
|
|
// ensureSQLiteDir creates the directory that will hold the SQLite file. It is a
|
|
// no-op for in-memory databases and for a bare filename in the current dir.
|
|
func ensureSQLiteDir(dsn string) error {
|
|
// Strip a "file:" prefix and any "?query" so we get the plain file path.
|
|
path := strings.TrimPrefix(dsn, "file:")
|
|
if i := strings.IndexByte(path, '?'); i >= 0 {
|
|
path = path[:i]
|
|
}
|
|
if path == "" || strings.Contains(path, ":memory:") {
|
|
return nil
|
|
}
|
|
dir := filepath.Dir(path)
|
|
if dir == "" || dir == "." {
|
|
return nil
|
|
}
|
|
return os.MkdirAll(dir, 0o755)
|
|
}
|
|
|
|
// Close releases the underlying connection pool.
|
|
func (s *Store) Close() error {
|
|
sqlDB, err := s.db.DB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sqlDB.Close()
|
|
}
|
|
|
|
// Ping verifies the database is reachable. Used by the health endpoint so
|
|
// Miabi can gate traffic until the app is actually ready.
|
|
func (s *Store) Ping(ctx context.Context) error {
|
|
sqlDB, err := s.db.DB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sqlDB.PingContext(ctx)
|
|
}
|
|
|
|
func (s *Store) Migrate() error {
|
|
return s.db.AutoMigrate(&Entry{})
|
|
}
|
|
|
|
// List returns entries newest first, with limit/offset pagination.
|
|
func (s *Store) List(ctx context.Context, limit, offset int) ([]Entry, error) {
|
|
if limit <= 0 || limit > 200 {
|
|
limit = 100
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
var entries []Entry
|
|
err := s.db.WithContext(ctx).
|
|
Order("created_at DESC").
|
|
Limit(limit).
|
|
Offset(offset).
|
|
Find(&entries).Error
|
|
return entries, err
|
|
}
|
|
|
|
// Create inserts a new entry and returns it with its generated id/timestamp.
|
|
func (s *Store) Create(ctx context.Context, name, message string) (Entry, error) {
|
|
e := Entry{Name: name, Message: message}
|
|
err := s.db.WithContext(ctx).Create(&e).Error
|
|
return e, err
|
|
}
|
|
|
|
// Delete soft-deletes an entry by id. Returns true when a row was affected.
|
|
func (s *Store) Delete(ctx context.Context, id uint64) (bool, error) {
|
|
res := s.db.WithContext(ctx).Delete(&Entry{}, id)
|
|
return res.RowsAffected > 0, res.Error
|
|
}
|
|
|
|
// Count returns the total number of (non-deleted) entries.
|
|
func (s *Store) Count(ctx context.Context) (int64, error) {
|
|
var n int64
|
|
err := s.db.WithContext(ctx).Model(&Entry{}).Count(&n).Error
|
|
return n, err
|
|
}
|