recents
import "github.com/Ask-Atlas/AskAtlas/api/internal/recents"
Package recents implements the GET /api/me/recents service surface (ASK-145): a per-viewer merged list of the most recently viewed files, study guides, and courses, used to power the sidebar "Recents" section.
The merge happens in the Go service rather than SQL (a UNION over three differently-shaped rows would force a lowest-common- denominator projection or per-row casting), which keeps each query hitting its own focused (user_id, viewed_at) index and lets the service map types into a discriminated-union domain shape.
Index
- Constants
- type EntityType
- type ListRecentsParams
- type ListRecentsResult
- type RecentCourseSummary
- type RecentFileSummary
- type RecentItem
- type RecentStudyGuideSummary
- type Repository
- type Service
Constants
const (
// DefaultLimit is applied when the caller does not specify a
// limit query parameter (or specifies 0). Matches the openapi.yaml
// default.
DefaultLimit int32 = 10
// MinLimit is the inclusive lower bound on limit. Matches openapi.yaml.
MinLimit int32 = 1
// MaxLimit is the inclusive upper bound on limit. Matches openapi.yaml.
MaxLimit int32 = 30
)
type EntityType
EntityType discriminates the populated payload on a RecentItem. The string values match the openapi enum exactly so the mapper can cast directly without a switch.
type EntityType string
const (
EntityTypeFile EntityType = "file"
EntityTypeStudyGuide EntityType = "study_guide"
EntityTypeCourse EntityType = "course"
)
type ListRecentsParams
ListRecentsParams is the input to Service.ListRecents. ViewerID is resolved from the Clerk JWT in the handler before this struct is constructed; the service never sees the JWT directly.
type ListRecentsParams struct {
ViewerID uuid.UUID
Limit int32
}
type ListRecentsResult
ListRecentsResult is the output of Service.ListRecents. A struct rather than a bare slice so future additions (an echoed limit, a computed total_view_count) can land backwards-compatibly.
type ListRecentsResult struct {
Recents []RecentItem
}
type RecentCourseSummary
RecentCourseSummary is the compact course payload embedded in a RecentItem when EntityType == EntityTypeCourse. Mirrors api.RecentCourseSummary on the wire.
type RecentCourseSummary struct {
ID uuid.UUID
Department string
Number string
Title string
}
type RecentFileSummary
RecentFileSummary is the compact file payload embedded in a RecentItem when EntityType == EntityTypeFile. Mirrors api.RecentFileSummary on the wire.
type RecentFileSummary struct {
ID uuid.UUID
Name string
MimeType string
}
type RecentItem
RecentItem is one merged recent row. Exactly one of File, StudyGuide, or Course is non-nil; EntityType declares which one. EntityID mirrors the populated summary's ID so callers can route off the envelope without unpacking the per-type payload.
Pointer fields (rather than a sealed-interface union) are chosen to match the existing codebase style and to round-trip cleanly through the api.RecentItem JSON struct (which uses *Summary fields with omitempty).
type RecentItem struct {
EntityType EntityType
EntityID uuid.UUID
ViewedAt time.Time
File *RecentFileSummary
StudyGuide *RecentStudyGuideSummary
Course *RecentCourseSummary
}
type RecentStudyGuideSummary
RecentStudyGuideSummary is the compact study-guide payload embedded in a RecentItem when EntityType == EntityTypeStudyGuide. Includes the parent course's department + number so the sidebar can render "CPTS 322 -- Binary Trees Cheat Sheet" without a follow-up request. Mirrors api.RecentStudyGuideSummary on the wire.
type RecentStudyGuideSummary struct {
ID uuid.UUID
Title string
CourseDepartment string
CourseNumber string
}
type Repository
Repository is the data-access surface required by the recents service. Three independent list calls keyed off the viewer; the service merges them in-process. Defined here (where it is used) rather than alongside db.Queries to keep the surface small and to allow mockery-generated mocks for service tests.
type Repository interface {
ListRecentFiles(ctx context.Context, arg db.ListRecentFilesParams) ([]db.ListRecentFilesRow, error)
ListRecentStudyGuides(ctx context.Context, arg db.ListRecentStudyGuidesParams) ([]db.ListRecentStudyGuidesRow, error)
ListRecentCourses(ctx context.Context, arg db.ListRecentCoursesParams) ([]db.ListRecentCoursesRow, error)
}
func NewSQLCRepository
func NewSQLCRepository(queries *db.Queries) Repository
NewSQLCRepository returns a Repository backed by sqlc-generated Postgres queries.
type Service
Service implements the GET /api/me/recents business logic.
type Service struct {
// contains filtered or unexported fields
}
func NewService
func NewService(repo Repository) *Service
NewService wires a recents Service over the given repository.
func (*Service) ListRecents
func (s *Service) ListRecents(ctx context.Context, p ListRecentsParams) (ListRecentsResult, error)
ListRecents returns the viewer's most recently viewed entities, merged across files, study guides, and courses (ASK-145).
Strategy:
- Fan out to three independent sqlc queries (one per entity type), each capped at the requested limit. A SQL UNION would force a lowest-common-denominator projection or per-row casting; in-process merge keeps each query hitting its own focused (user_id, viewed_at) index and lets the mapper produce a discriminated-union shape.
- Sort the combined slice by ViewedAt DESC, breaking ties on EntityID lexicographic so the output is deterministic when two views landed in the same microsecond.
- Truncate to limit. Each per-type query already returned at most `limit` rows, so the unsorted combined slice has at most 3*limit entries -- the merge is O(N log N) in N=3*limit, which is a negligible amount of work at MaxLimit=30 (90 items).
Limit is applied per-query AND post-merge because either bound alone is wrong: a per-query bound alone returns up to 3*limit items when the caller asked for limit; a post-merge bound alone would require fetching every row from each table to guarantee the limit-most-recent global window. Both bounds together are correct and bounded.
Generated by gomarkdoc