Initial commit
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user