Contains: - db.go: Database connection wrapper with helper methods - models.go: Domain, Feed, Item, ShortURL, Click structs - util.go: URL normalization, TLD functions, search helpers - handle.go: AT Protocol handle derivation from feed URLs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
157 lines
3.9 KiB
Go
157 lines
3.9 KiB
Go
package shared
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// DB wraps pgxpool.Pool with helper methods
|
|
type DB struct {
|
|
*pgxpool.Pool
|
|
}
|
|
|
|
// OpenDatabase connects to PostgreSQL using environment variables or connection string
|
|
func OpenDatabase(connString string) (*DB, error) {
|
|
fmt.Printf("Connecting to database...\n")
|
|
|
|
// If connection string not provided, try environment variables
|
|
if connString == "" {
|
|
connString = os.Getenv("DATABASE_URL")
|
|
}
|
|
if connString == "" {
|
|
// Build from individual env vars
|
|
host := GetEnvOrDefault("DB_HOST", "infra-postgres")
|
|
port := GetEnvOrDefault("DB_PORT", "5432")
|
|
user := GetEnvOrDefault("DB_USER", "dba_1440_news")
|
|
dbname := GetEnvOrDefault("DB_NAME", "db_1440_news")
|
|
|
|
// Support Docker secrets (password file) or direct password
|
|
password := os.Getenv("DB_PASSWORD")
|
|
if password == "" {
|
|
if passwordFile := os.Getenv("DB_PASSWORD_FILE"); passwordFile != "" {
|
|
data, err := os.ReadFile(passwordFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read password file: %v", err)
|
|
}
|
|
password = strings.TrimSpace(string(data))
|
|
}
|
|
}
|
|
|
|
connString = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
|
|
user, url.QueryEscape(password), host, port, dbname)
|
|
}
|
|
|
|
config, err := pgxpool.ParseConfig(connString)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse connection string: %v", err)
|
|
}
|
|
|
|
// Connection pool settings
|
|
config.MaxConns = 10
|
|
config.MinConns = 0 // Don't pre-create connections to avoid schema race conditions
|
|
config.MaxConnLifetime = 5 * time.Minute
|
|
config.MaxConnIdleTime = 1 * time.Minute
|
|
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.NewWithConfig(ctx, config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to database: %v", err)
|
|
}
|
|
|
|
// Verify connection
|
|
if err := pool.Ping(ctx); err != nil {
|
|
pool.Close()
|
|
return nil, fmt.Errorf("failed to ping database: %v", err)
|
|
}
|
|
fmt.Println(" Connected to PostgreSQL")
|
|
|
|
return &DB{pool}, nil
|
|
}
|
|
|
|
// GetEnvOrDefault returns environment variable value or default
|
|
func GetEnvOrDefault(key, defaultVal string) string {
|
|
if val := os.Getenv(key); val != "" {
|
|
return val
|
|
}
|
|
return defaultVal
|
|
}
|
|
|
|
// QueryRow wraps pool.QueryRow for compatibility
|
|
func (db *DB) QueryRow(query string, args ...interface{}) pgx.Row {
|
|
return db.Pool.QueryRow(context.Background(), query, args...)
|
|
}
|
|
|
|
// Query wraps pool.Query for compatibility
|
|
func (db *DB) Query(query string, args ...interface{}) (pgx.Rows, error) {
|
|
return db.Pool.Query(context.Background(), query, args...)
|
|
}
|
|
|
|
// Exec wraps pool.Exec for compatibility
|
|
func (db *DB) Exec(query string, args ...interface{}) (int64, error) {
|
|
result, err := db.Pool.Exec(context.Background(), query, args...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
// Begin starts a transaction
|
|
func (db *DB) Begin() (pgx.Tx, error) {
|
|
return db.Pool.Begin(context.Background())
|
|
}
|
|
|
|
// Close closes the connection pool
|
|
func (db *DB) Close() error {
|
|
db.Pool.Close()
|
|
return nil
|
|
}
|
|
|
|
// NullableString returns nil for empty strings, otherwise the string pointer
|
|
func NullableString(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|
|
|
|
// NullableTime returns nil for zero times, otherwise the time pointer
|
|
func NullableTime(t time.Time) *time.Time {
|
|
if t.IsZero() {
|
|
return nil
|
|
}
|
|
return &t
|
|
}
|
|
|
|
// StringValue returns empty string for nil, otherwise the dereferenced value
|
|
func StringValue(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
// TimeValue returns zero time for nil, otherwise the dereferenced value
|
|
func TimeValue(t *time.Time) time.Time {
|
|
if t == nil {
|
|
return time.Time{}
|
|
}
|
|
return *t
|
|
}
|
|
|
|
// ToSearchQuery converts a user query to PostgreSQL tsquery format
|
|
func ToSearchQuery(query string) string {
|
|
// Simple conversion: split on spaces and join with &
|
|
words := strings.Fields(query)
|
|
if len(words) == 0 {
|
|
return ""
|
|
}
|
|
return strings.Join(words, " & ")
|
|
}
|