Files
spsays-bff/internal/repository/stats_repo.go
T

85 lines
3.1 KiB
Go

package repository
import (
"context"
"fmt"
"time"
)
// StatsRepo runs the aggregate count queries that power /stats. Lives
// in its own file rather than spreading COUNT methods across every
// domain repo — keeps the at-a-glance "system snapshot" queries in
// one place and one trip into the pool.
type StatsRepo struct{ pg *Postgres }
func NewStatsRepo(pg *Postgres) *StatsRepo { return &StatsRepo{pg: pg} }
// StatsSnapshot bundles totals, last-7-days counts, and feedback
// category breakdowns. One value rendered by /stats per request.
type StatsSnapshot struct {
Users int `json:"users"`
UsersLast7Days int `json:"users_last_7_days"`
Posts int `json:"posts"`
PostsLast7Days int `json:"posts_last_7_days"`
Collections int `json:"collections"`
PublicCollections int `json:"public_collections"`
Bookmarks int `json:"bookmarks"`
Reshares int `json:"reshares"`
Feedback int `json:"feedback"`
FeedbackLast7Days int `json:"feedback_last_7_days"`
FeedbackByCategory []CategoryCount `json:"feedback_by_category"`
}
type CategoryCount struct {
Category string `json:"category"`
Count int `json:"count"`
}
// Snapshot runs every count needed by /stats. Queries run in series —
// the table sizes are modest enough that chasing parallelism here
// would buy sub-ms wins for added complexity.
func (r *StatsRepo) Snapshot(ctx context.Context) (*StatsSnapshot, error) {
s := &StatsSnapshot{}
sevenAgo := time.Now().Add(-7 * 24 * time.Hour)
scalars := []struct {
sql string
args []any
dst *int
}{
{`SELECT COUNT(*) FROM users`, nil, &s.Users},
{`SELECT COUNT(*) FROM users WHERE created_at >= $1`, []any{sevenAgo}, &s.UsersLast7Days},
{`SELECT COUNT(*) FROM posts`, nil, &s.Posts},
{`SELECT COUNT(*) FROM posts WHERE created_at >= $1`, []any{sevenAgo}, &s.PostsLast7Days},
{`SELECT COUNT(*) FROM collections`, nil, &s.Collections},
{`SELECT COUNT(*) FROM collections WHERE is_public`, nil, &s.PublicCollections},
{`SELECT COUNT(*) FROM bookmarks`, nil, &s.Bookmarks},
{`SELECT COUNT(*) FROM posts WHERE reshared_from_post_id IS NOT NULL`, nil, &s.Reshares},
{`SELECT COUNT(*) FROM feedback`, nil, &s.Feedback},
{`SELECT COUNT(*) FROM feedback WHERE created_at >= $1`, []any{sevenAgo}, &s.FeedbackLast7Days},
}
for _, q := range scalars {
if err := r.pg.Pool.QueryRow(ctx, q.sql, q.args...).Scan(q.dst); err != nil {
return nil, fmt.Errorf("stats scalar %q: %w", q.sql, err)
}
}
rows, err := r.pg.Pool.Query(ctx, `
SELECT category, COUNT(*)
FROM feedback
GROUP BY category
ORDER BY COUNT(*) DESC, category ASC`)
if err != nil {
return nil, fmt.Errorf("feedback by category: %w", err)
}
defer rows.Close()
for rows.Next() {
var cc CategoryCount
if err := rows.Scan(&cc.Category, &cc.Count); err != nil {
return nil, fmt.Errorf("scan category: %w", err)
}
s.FeedbackByCategory = append(s.FeedbackByCategory, cc)
}
return s, rows.Err()
}