85 lines
3.1 KiB
Go
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()
|
|
}
|