Initial lookbook implementation
Pinterest-style visual bookmarking app with: - URL metadata extraction (OG/Twitter meta, oEmbed fallback) - Image caching in Postgres with 480px thumbnails - Multi-tag filtering with Ctrl/Cmd for OR mode - Fuzzy tag suggestions and inline tag editing - Browser console auth() with first-use password setup - Brutalist UI with Commit Mono font and Pico CSS - Light/dark mode via browser preference
This commit is contained in:
commit
fc625fb9cf
486 changed files with 195373 additions and 0 deletions
130
internal/migrations/migrations.go
Normal file
130
internal/migrations/migrations.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
//go:embed sql/*.sql
|
||||
var FS embed.FS
|
||||
|
||||
const DefaultURL = "postgres://postgres:postgres@localhost:5432/lookbook?sslmode=disable"
|
||||
|
||||
// Up applies all available migrations using the provided database URL.
|
||||
func Up(ctx context.Context, dbURL string, logger *slog.Logger) error {
|
||||
url := dbURL
|
||||
if url == "" {
|
||||
url = DefaultURL
|
||||
}
|
||||
|
||||
db, err := openDB(url, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := goose.UpContext(ctx, db, "sql"); err != nil {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("database migrated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Down rolls back migrations. If targetVersion < 0, it steps back one migration; otherwise it migrates down to the target version.
|
||||
func Down(ctx context.Context, dbURL string, targetVersion int64, logger *slog.Logger) error {
|
||||
url := dbURL
|
||||
if url == "" {
|
||||
url = DefaultURL
|
||||
}
|
||||
|
||||
db, err := openDB(url, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if targetVersion < 0 {
|
||||
if err := goose.DownContext(ctx, db, "sql"); err != nil {
|
||||
return fmt.Errorf("rollback one: %w", err)
|
||||
}
|
||||
logger.Info("rolled back one migration")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := goose.DownToContext(ctx, db, "sql", targetVersion); err != nil {
|
||||
return fmt.Errorf("rollback to version %d: %w", targetVersion, err)
|
||||
}
|
||||
logger.Info("rolled back to version", slog.Int64("version", targetVersion))
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckPending(ctx context.Context, dbURL string, logger *slog.Logger) (int, error) {
|
||||
url := dbURL
|
||||
if url == "" {
|
||||
url = DefaultURL
|
||||
}
|
||||
|
||||
db, err := openDB(url, logger)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
current, err := goose.GetDBVersionContext(ctx, db)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get db version: %w", err)
|
||||
}
|
||||
|
||||
migrations, err := goose.CollectMigrations("sql", 0, goose.MaxVersion)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("collect migrations: %w", err)
|
||||
}
|
||||
|
||||
pending := 0
|
||||
for _, m := range migrations {
|
||||
if m.Version > current {
|
||||
pending++
|
||||
}
|
||||
}
|
||||
|
||||
return pending, nil
|
||||
}
|
||||
|
||||
// slogLogger adapts slog to goose's minimal logging interface.
|
||||
type slogLogger struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (l slogLogger) Printf(format string, v ...any) {
|
||||
l.logger.Info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (l slogLogger) Fatalf(format string, v ...any) {
|
||||
l.logger.Error(fmt.Sprintf(format, v...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func openDB(url string, logger *slog.Logger) (*sql.DB, error) {
|
||||
goose.SetBaseFS(FS)
|
||||
goose.SetLogger(slogLogger{logger: logger})
|
||||
|
||||
db, err := sql.Open("pgx", url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("ping db: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue