Include nested lesson and practice counts in exam-prep modules list response.

Return per-module lesson and practice aggregates under unit modules listing so clients can render module depth without additional queries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-05 07:05:35 -07:00
parent 16c3f6b613
commit b62d89574e
4 changed files with 56 additions and 21 deletions

View File

@ -30,6 +30,16 @@ ORDER BY
m.id; m.id;
-- name: ExamPrepListUnitModulesByUnit :many -- name: ExamPrepListUnitModulesByUnit :many
WITH module_counts AS (
SELECT
m.id AS module_id,
COUNT(DISTINCT l.id)::BIGINT AS lessons_count,
COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.unit_modules m
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
GROUP BY m.id
)
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
m.id, m.id,
@ -39,9 +49,12 @@ SELECT
m.thumbnail, m.thumbnail,
m.icon, m.icon,
m.sort_order, m.sort_order,
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
m.created_at, m.created_at,
m.updated_at m.updated_at
FROM exam_prep.unit_modules m FROM exam_prep.unit_modules m
LEFT JOIN module_counts mc ON mc.module_id = m.id
WHERE WHERE
m.unit_id = $1 m.unit_id = $1
ORDER BY ORDER BY

View File

@ -124,6 +124,16 @@ func (q *Queries) ExamPrepListUnitModuleIDsByUnit(ctx context.Context, unitID in
} }
const ExamPrepListUnitModulesByUnit = `-- name: ExamPrepListUnitModulesByUnit :many const ExamPrepListUnitModulesByUnit = `-- name: ExamPrepListUnitModulesByUnit :many
WITH module_counts AS (
SELECT
m.id AS module_id,
COUNT(DISTINCT l.id)::BIGINT AS lessons_count,
COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.unit_modules m
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
GROUP BY m.id
)
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
m.id, m.id,
@ -133,9 +143,12 @@ SELECT
m.thumbnail, m.thumbnail,
m.icon, m.icon,
m.sort_order, m.sort_order,
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
m.created_at, m.created_at,
m.updated_at m.updated_at
FROM exam_prep.unit_modules m FROM exam_prep.unit_modules m
LEFT JOIN module_counts mc ON mc.module_id = m.id
WHERE WHERE
m.unit_id = $1 m.unit_id = $1
ORDER BY ORDER BY
@ -152,16 +165,18 @@ type ExamPrepListUnitModulesByUnitParams struct {
} }
type ExamPrepListUnitModulesByUnitRow struct { type ExamPrepListUnitModulesByUnitRow struct {
TotalCount int64 `json:"total_count"` TotalCount int64 `json:"total_count"`
ID int64 `json:"id"` ID int64 `json:"id"`
UnitID int64 `json:"unit_id"` UnitID int64 `json:"unit_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` LessonsCount int64 `json:"lessons_count"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` PracticesCount int64 `json:"practices_count"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPrepListUnitModulesByUnitParams) ([]ExamPrepListUnitModulesByUnitRow, error) { func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPrepListUnitModulesByUnitParams) ([]ExamPrepListUnitModulesByUnitRow, error) {
@ -182,6 +197,8 @@ func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPre
&i.Thumbnail, &i.Thumbnail,
&i.Icon, &i.Icon,
&i.SortOrder, &i.SortOrder,
&i.LessonsCount,
&i.PracticesCount,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {

View File

@ -4,15 +4,17 @@ import "time"
// ExamPrepModule is a module under an exam-prep unit (stored in exam_prep.unit_modules). // ExamPrepModule is a module under an exam-prep unit (stored in exam_prep.unit_modules).
type ExamPrepModule struct { type ExamPrepModule struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UnitID int64 `json:"unit_id"` UnitID int64 `json:"unit_id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"` LessonsCount *int64 `json:"lessons_count,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }
type CreateExamPrepModuleInput struct { type CreateExamPrepModuleInput struct {

View File

@ -72,7 +72,7 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64,
if i == 0 { if i == 0 {
total = r.TotalCount total = r.TotalCount
} }
out = append(out, examPrepModuleToDomain(dbgen.ExamPrepUnitModule{ item := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{
ID: r.ID, ID: r.ID,
UnitID: r.UnitID, UnitID: r.UnitID,
Name: r.Name, Name: r.Name,
@ -82,7 +82,10 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
})) })
item.LessonsCount = &r.LessonsCount
item.PracticesCount = &r.PracticesCount
out = append(out, item)
} }
return out, total, nil return out, total, nil
} }