feat: P1 profile/portrait API and freeze local-dev environment
Add host-first environment contracts (Local vs CI vs Prod), deps-only compose, and the Profile → Portrait → deep-access mock payment slice with device identity and auto-migrate on API startup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// Package db opens Postgres and runs SQL migrations.
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Connect opens a pgx pool using DATABASE_URL-style DSN.
|
||||
func Connect(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pgxpool: %w", err)
|
||||
}
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("ping: %w", err)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// Migrate applies *.up.sql files under dir that are not yet recorded.
|
||||
func Migrate(ctx context.Context, pool *pgxpool.Pool, dir string) error {
|
||||
if _, err := pool.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version text PRIMARY KEY,
|
||||
applied_at timestamptz NOT NULL DEFAULT now()
|
||||
)`); err != nil {
|
||||
return fmt.Errorf("schema_migrations: %w", err)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read migrations: %w", err)
|
||||
}
|
||||
var ups []string
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if strings.HasSuffix(name, ".up.sql") {
|
||||
ups = append(ups, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(ups)
|
||||
|
||||
for _, name := range ups {
|
||||
version := strings.TrimSuffix(name, ".up.sql")
|
||||
var exists bool
|
||||
if err := pool.QueryRow(ctx,
|
||||
`SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=$1)`, version,
|
||||
).Scan(&exists); err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
body, err := os.ReadFile(filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// DDL (incl. CREATE EXTENSION) may not run inside a transaction.
|
||||
if _, err := pool.Exec(ctx, string(body)); err != nil {
|
||||
return fmt.Errorf("migrate %s: %w", name, err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx,
|
||||
`INSERT INTO schema_migrations(version) VALUES ($1)`, version,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user