feat(store): route all DB access through withUser for structural scoping
Add Store.withUser(ctx, userID, fn) — a single choke point that BEGINs a tx, sets the transaction-local GUC tapir.current_user_id via set_config(..., true), runs fn, and commits. set_config is used over SET LOCAL because it is parameterizable; the local flag means the value auto-resets on commit/rollback so a pooled connection never leaks one request's user into the next. Route all 9 DB-touching methods through it (Deliver, HasSummary, SeenVideoIDs, ListSummaries, GetSummaryByVideo, SetAction, ClearAction, ActionsFor, and UpsertVideo; attachActions flows via ActionsFor). Scoping is now structural — not a per-query opt-in someone can forget — and arms the migration-003 RLS policies. Method signatures and existing WHERE clauses are unchanged (defence in depth; superuser DSNs in existing tests bypass RLS so behaviour is preserved). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -66,27 +66,32 @@ func (s *Store) ListSummaries(ctx context.Context, userID string, limit int) ([]
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := s.pool.Query(ctx,
|
||||
selectSummary+`
|
||||
WHERE s.user_id = $1
|
||||
ORDER BY s.created_at DESC
|
||||
LIMIT $2`,
|
||||
userID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: list summaries: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []SummaryRow
|
||||
for rows.Next() {
|
||||
row, err := scanSummaryRow(rows)
|
||||
if err := s.withUser(ctx, userID, func(tx pgx.Tx) error {
|
||||
rows, err := tx.Query(ctx,
|
||||
selectSummary+`
|
||||
WHERE s.user_id = $1
|
||||
ORDER BY s.created_at DESC
|
||||
LIMIT $2`,
|
||||
userID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("store: list summaries: %w", err)
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("store: iterate summaries: %w", err)
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
row, err := scanSummaryRow(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("store: iterate summaries: %w", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.attachActions(ctx, userID, out); err != nil {
|
||||
return nil, err
|
||||
@@ -98,25 +103,38 @@ func (s *Store) ListSummaries(ctx context.Context, userID string, limit int) ([]
|
||||
// highlights and takeaways. Returns ErrNotFound when the user has no such
|
||||
// summary. Scoped by user_id.
|
||||
func (s *Store) GetSummaryByVideo(ctx context.Context, userID, videoID string) (*SummaryRow, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
selectSummary+`
|
||||
WHERE s.user_id = $1 AND s.video_id = $2`,
|
||||
userID, videoID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: get summary: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("store: get summary: %w", err)
|
||||
var (
|
||||
row SummaryRow
|
||||
found bool
|
||||
)
|
||||
if err := s.withUser(ctx, userID, func(tx pgx.Tx) error {
|
||||
rows, err := tx.Query(ctx,
|
||||
selectSummary+`
|
||||
WHERE s.user_id = $1 AND s.video_id = $2`,
|
||||
userID, videoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("store: get summary: %w", err)
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
row, err := scanSummaryRow(rows)
|
||||
if err != nil {
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("store: get summary: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
row, err = scanSummaryRow(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found = true
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
holder := []SummaryRow{row}
|
||||
if err := s.attachActions(ctx, userID, holder); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user