···
11
+
type postgresUnfurlRepo struct {
15
+
// NewRepository creates a new PostgreSQL unfurl cache repository
16
+
func NewRepository(db *sql.DB) Repository {
17
+
return &postgresUnfurlRepo{db: db}
20
+
// Get retrieves a cached unfurl result for the given URL.
21
+
// Returns nil, nil if not found or expired (not an error condition).
22
+
// Returns error only on database failures.
23
+
func (r *postgresUnfurlRepo) Get(ctx context.Context, url string) (*UnfurlResult, error) {
25
+
SELECT metadata, thumbnail_url, provider
27
+
WHERE url = $1 AND expires_at > NOW()
30
+
var metadataJSON []byte
31
+
var thumbnailURL sql.NullString
34
+
err := r.db.QueryRowContext(ctx, query, url).Scan(&metadataJSON, &thumbnailURL, &provider)
35
+
if err == sql.ErrNoRows {
36
+
// Not found or expired is not an error
40
+
return nil, fmt.Errorf("failed to get unfurl cache entry: %w", err)
43
+
// Unmarshal metadata JSONB to UnfurlResult
44
+
var result UnfurlResult
45
+
if err := json.Unmarshal(metadataJSON, &result); err != nil {
46
+
return nil, fmt.Errorf("failed to unmarshal metadata: %w", err)
49
+
// Ensure provider and thumbnailURL are set (may not be in metadata JSON)
50
+
result.Provider = provider
51
+
if thumbnailURL.Valid {
52
+
result.ThumbnailURL = thumbnailURL.String
58
+
// Set stores an unfurl result in the cache with the specified TTL.
59
+
// If an entry already exists for the URL, it will be updated.
60
+
// The expires_at is calculated as NOW() + ttl.
61
+
func (r *postgresUnfurlRepo) Set(ctx context.Context, url string, result *UnfurlResult, ttl time.Duration) error {
62
+
// Marshal UnfurlResult to JSON for metadata column
63
+
metadataJSON, err := json.Marshal(result)
65
+
return fmt.Errorf("failed to marshal metadata: %w", err)
68
+
// Store thumbnail_url separately for potential queries
69
+
var thumbnailURL sql.NullString
70
+
if result.ThumbnailURL != "" {
71
+
thumbnailURL.String = result.ThumbnailURL
72
+
thumbnailURL.Valid = true
75
+
// Convert Go duration to PostgreSQL interval string
76
+
// e.g., "1 hour", "24 hours", "7 days"
77
+
intervalStr := formatInterval(ttl)
80
+
INSERT INTO unfurl_cache (url, provider, metadata, thumbnail_url, expires_at)
81
+
VALUES ($1, $2, $3, $4, NOW() + $5::interval)
82
+
ON CONFLICT (url) DO UPDATE
83
+
SET provider = EXCLUDED.provider,
84
+
metadata = EXCLUDED.metadata,
85
+
thumbnail_url = EXCLUDED.thumbnail_url,
86
+
expires_at = EXCLUDED.expires_at,
90
+
_, err = r.db.ExecContext(ctx, query, url, result.Provider, metadataJSON, thumbnailURL, intervalStr)
92
+
return fmt.Errorf("failed to insert/update unfurl cache entry: %w", err)
98
+
// formatInterval converts a Go duration to a PostgreSQL interval string
99
+
// PostgreSQL accepts intervals like "1 hour", "24 hours", "7 days"
100
+
func formatInterval(d time.Duration) string {
101
+
seconds := int64(d.Seconds())
103
+
// Convert to appropriate unit for readability
105
+
case seconds >= 86400: // >= 1 day
106
+
days := seconds / 86400
107
+
return fmt.Sprintf("%d days", days)
108
+
case seconds >= 3600: // >= 1 hour
109
+
hours := seconds / 3600
110
+
return fmt.Sprintf("%d hours", hours)
111
+
case seconds >= 60: // >= 1 minute
112
+
minutes := seconds / 60
113
+
return fmt.Sprintf("%d minutes", minutes)
115
+
return fmt.Sprintf("%d seconds", seconds)