package db import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "strings" "time" ) type ArticleSummary struct { ID int64 `json:"id"` URLHash string `json:"urlHash"` URL string `json:"url"` Events []string `json:"events"` CreatedAt time.Time `json:"createdAt"` ExpiresAt time.Time `json:"expiresAt"` } type ArticleSummaryRepository struct { db *PostgresDB } func NewArticleSummaryRepository(db *PostgresDB) *ArticleSummaryRepository { return &ArticleSummaryRepository{db: db} } func (r *ArticleSummaryRepository) hashURL(url string) string { normalized := strings.TrimSpace(url) normalized = strings.TrimSuffix(normalized, "/") normalized = strings.TrimPrefix(normalized, "https://") normalized = strings.TrimPrefix(normalized, "http://") normalized = strings.TrimPrefix(normalized, "www.") hash := sha256.Sum256([]byte(normalized)) return hex.EncodeToString(hash[:]) } func (r *ArticleSummaryRepository) GetByURL(ctx context.Context, url string) (*ArticleSummary, error) { urlHash := r.hashURL(url) query := ` SELECT id, url_hash, url, events, created_at, expires_at FROM article_summaries WHERE url_hash = $1 AND expires_at > NOW() ` var a ArticleSummary var eventsJSON []byte err := r.db.db.QueryRowContext(ctx, query, urlHash).Scan( &a.ID, &a.URLHash, &a.URL, &eventsJSON, &a.CreatedAt, &a.ExpiresAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } json.Unmarshal(eventsJSON, &a.Events) return &a, nil } func (r *ArticleSummaryRepository) Save(ctx context.Context, url string, events []string, ttl time.Duration) error { urlHash := r.hashURL(url) eventsJSON, _ := json.Marshal(events) expiresAt := time.Now().Add(ttl) query := ` INSERT INTO article_summaries (url_hash, url, events, expires_at) VALUES ($1, $2, $3, $4) ON CONFLICT (url_hash) DO UPDATE SET events = EXCLUDED.events, expires_at = EXCLUDED.expires_at ` _, err := r.db.db.ExecContext(ctx, query, urlHash, url, eventsJSON, expiresAt) return err } func (r *ArticleSummaryRepository) Delete(ctx context.Context, url string) error { urlHash := r.hashURL(url) _, err := r.db.db.ExecContext(ctx, "DELETE FROM article_summaries WHERE url_hash = $1", urlHash) return err } func (r *ArticleSummaryRepository) CleanupExpired(ctx context.Context) (int64, error) { result, err := r.db.db.ExecContext(ctx, "DELETE FROM article_summaries WHERE expires_at < NOW()") if err != nil { return 0, err } return result.RowsAffected() }