package repository import ( "context" "errors" "fmt" "strings" "github.com/jackc/pgx/v5" "github.com/prabhupadasays/spsays-bff/internal/model" ) var ErrBookmarkNotFound = errors.New("bookmark not found") var ErrBookmarkExists = errors.New("bookmark already exists for this URL") type BookmarkRepo struct{ pg *Postgres } func NewBookmarkRepo(pg *Postgres) *BookmarkRepo { return &BookmarkRepo{pg: pg} } // Create inserts a new bookmark. ErrBookmarkExists on (user_id, source_url) collision. func (r *BookmarkRepo) Create(ctx context.Context, userID, sourceURL, sourceTitle string, excerpt, note *string) (*model.Bookmark, error) { b := &model.Bookmark{} err := r.pg.Pool.QueryRow(ctx, ` INSERT INTO bookmarks (user_id, source_url, source_title, excerpt, note) VALUES ($1, $2, $3, $4, $5) RETURNING id, user_id, source_url, COALESCE(source_title, ''), excerpt, note, created_at`, userID, sourceURL, sourceTitle, excerpt, note, ).Scan(&b.ID, &b.UserID, &b.SourceURL, &b.SourceTitle, &b.Excerpt, &b.Note, &b.CreatedAt) if err != nil { if strings.Contains(err.Error(), "23505") { return nil, ErrBookmarkExists } return nil, fmt.Errorf("create bookmark: %w", err) } return b, nil } // ListByUser returns all bookmarks for the caller, newest first. func (r *BookmarkRepo) ListByUser(ctx context.Context, userID string) ([]model.Bookmark, error) { rows, err := r.pg.Pool.Query(ctx, ` SELECT id, user_id, source_url, COALESCE(source_title, ''), excerpt, note, created_at FROM bookmarks WHERE user_id = $1 ORDER BY created_at DESC`, userID) if err != nil { return nil, fmt.Errorf("query bookmarks: %w", err) } defer rows.Close() out := []model.Bookmark{} for rows.Next() { var b model.Bookmark if err := rows.Scan(&b.ID, &b.UserID, &b.SourceURL, &b.SourceTitle, &b.Excerpt, &b.Note, &b.CreatedAt); err != nil { return nil, fmt.Errorf("scan bookmark: %w", err) } out = append(out, b) } return out, rows.Err() } // CountByUser is used by the plan-limit enforcer. func (r *BookmarkRepo) CountByUser(ctx context.Context, userID string) (int, error) { var n int err := r.pg.Pool.QueryRow(ctx, `SELECT COUNT(*) FROM bookmarks WHERE user_id = $1`, userID).Scan(&n) if err != nil { return 0, fmt.Errorf("count bookmarks: %w", err) } return n, nil } // Delete removes a bookmark belonging to the caller. func (r *BookmarkRepo) Delete(ctx context.Context, id, userID string) error { tag, err := r.pg.Pool.Exec(ctx, `DELETE FROM bookmarks WHERE id = $1 AND user_id = $2`, id, userID) if err != nil { return fmt.Errorf("delete bookmark: %w", err) } if tag.RowsAffected() == 0 { return ErrBookmarkNotFound } return nil } // FindByID returns one bookmark (used for the owner-check path). func (r *BookmarkRepo) FindByID(ctx context.Context, id string) (*model.Bookmark, error) { b := &model.Bookmark{} err := r.pg.Pool.QueryRow(ctx, ` SELECT id, user_id, source_url, COALESCE(source_title, ''), excerpt, note, created_at FROM bookmarks WHERE id = $1`, id, ).Scan(&b.ID, &b.UserID, &b.SourceURL, &b.SourceTitle, &b.Excerpt, &b.Note, &b.CreatedAt) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrBookmarkNotFound } return nil, fmt.Errorf("find bookmark: %w", err) } return b, nil }