Expand course hierarchy read APIs and practice retrieval.

Add list/detail endpoints for courses, levels, modules, submodules, and submodule practices; extend course listing queries; add lesson update support and clean up removed route paths.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-17 07:52:22 -07:00
parent 343ce470cc
commit 1026354c24
6 changed files with 1175 additions and 2 deletions

View File

@ -33,6 +33,57 @@ ORDER BY display_order ASC, id ASC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetAllCourses :many
SELECT
COUNT(*) OVER () AS total_count,
c.id,
c.category_id,
c.sub_category_id,
c.title,
c.description,
c.thumbnail,
c.intro_video_url,
c.is_active
FROM courses c
ORDER BY c.display_order ASC, c.id ASC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetHumanLanguageCourses :many
SELECT
COUNT(*) OVER () AS total_count,
c.id,
c.category_id,
c.sub_category_id,
c.title,
c.description,
c.thumbnail,
c.intro_video_url,
c.is_active
FROM courses c
JOIN course_categories cc ON cc.id = c.category_id
WHERE lower(trim(cc.name)) = 'human language'
ORDER BY c.display_order ASC, c.id ASC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetCoursesBySubCategory :many
SELECT
COUNT(*) OVER () AS total_count,
c.id,
c.category_id,
c.sub_category_id,
c.title,
c.description,
c.thumbnail,
c.intro_video_url,
c.is_active
FROM courses c
WHERE c.sub_category_id = $1
ORDER BY c.display_order ASC, c.id ASC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: UpdateCourse :exec
UPDATE courses

View File

@ -19,6 +19,20 @@ WHERE course_id = $1
AND is_active = TRUE
ORDER BY display_order ASC, id ASC;
-- name: GetAllLevels :many
SELECT
COUNT(*) OVER () AS total_count,
l.*
FROM levels l
ORDER BY l.display_order ASC, l.id ASC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetLevelByID :one
SELECT *
FROM levels
WHERE id = $1;
-- name: GetModulesByLevelID :many
SELECT *
FROM modules
@ -26,6 +40,20 @@ WHERE level_id = $1
AND is_active = TRUE
ORDER BY display_order ASC, id ASC;
-- name: GetAllModules :many
SELECT
COUNT(*) OVER () AS total_count,
m.*
FROM modules m
ORDER BY m.display_order ASC, m.id ASC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetModuleByID :one
SELECT *
FROM modules
WHERE id = $1;
-- name: GetSubModulesByModuleID :many
SELECT *
FROM sub_modules
@ -33,6 +61,20 @@ WHERE module_id = $1
AND is_active = TRUE
ORDER BY display_order ASC, id ASC;
-- name: GetAllSubModules :many
SELECT
COUNT(*) OVER () AS total_count,
sm.*
FROM sub_modules sm
ORDER BY sm.display_order ASC, sm.id ASC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetSubModuleByID :one
SELECT *
FROM sub_modules
WHERE id = $1;
-- name: GetSubModuleVideos :many
SELECT *
FROM sub_module_videos
@ -100,6 +142,26 @@ WHERE smp.sub_module_id = $1
AND qs.set_type = 'PRACTICE'
ORDER BY smp.display_order ASC, smp.id ASC;
-- name: GetSubModulePracticeByID :one
SELECT
smp.id,
smp.sub_module_id,
smp.title,
smp.description,
smp.thumbnail,
smp.intro_video_url,
smp.question_set_id,
smp.display_order,
smp.is_active,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_practices smp
JOIN question_sets qs ON qs.id = smp.question_set_id
WHERE smp.id = $1
AND smp.is_active = TRUE
AND qs.set_type = 'PRACTICE';
-- name: GetFullHierarchyByCourseID :many
SELECT
c.id AS course_id,

View File

@ -67,6 +67,70 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
return err
}
const GetAllCourses = `-- name: GetAllCourses :many
SELECT
COUNT(*) OVER () AS total_count,
c.id,
c.category_id,
c.sub_category_id,
c.title,
c.description,
c.thumbnail,
c.intro_video_url,
c.is_active
FROM courses c
ORDER BY c.display_order ASC, c.id ASC
LIMIT $2::INT
OFFSET $1::INT
`
type GetAllCoursesParams struct {
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetAllCoursesRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
CategoryID int64 `json:"category_id"`
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
IsActive bool `json:"is_active"`
}
func (q *Queries) GetAllCourses(ctx context.Context, arg GetAllCoursesParams) ([]GetAllCoursesRow, error) {
rows, err := q.db.Query(ctx, GetAllCourses, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllCoursesRow
for rows.Next() {
var i GetAllCoursesRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.CategoryID,
&i.SubCategoryID,
&i.Title,
&i.Description,
&i.Thumbnail,
&i.IntroVideoUrl,
&i.IsActive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCourseByID = `-- name: GetCourseByID :one
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
FROM courses
@ -153,6 +217,138 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
return items, nil
}
const GetCoursesBySubCategory = `-- name: GetCoursesBySubCategory :many
SELECT
COUNT(*) OVER () AS total_count,
c.id,
c.category_id,
c.sub_category_id,
c.title,
c.description,
c.thumbnail,
c.intro_video_url,
c.is_active
FROM courses c
WHERE c.sub_category_id = $1
ORDER BY c.display_order ASC, c.id ASC
LIMIT $3::INT
OFFSET $2::INT
`
type GetCoursesBySubCategoryParams struct {
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetCoursesBySubCategoryRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
CategoryID int64 `json:"category_id"`
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
IsActive bool `json:"is_active"`
}
func (q *Queries) GetCoursesBySubCategory(ctx context.Context, arg GetCoursesBySubCategoryParams) ([]GetCoursesBySubCategoryRow, error) {
rows, err := q.db.Query(ctx, GetCoursesBySubCategory, arg.SubCategoryID, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCoursesBySubCategoryRow
for rows.Next() {
var i GetCoursesBySubCategoryRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.CategoryID,
&i.SubCategoryID,
&i.Title,
&i.Description,
&i.Thumbnail,
&i.IntroVideoUrl,
&i.IsActive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetHumanLanguageCourses = `-- name: GetHumanLanguageCourses :many
SELECT
COUNT(*) OVER () AS total_count,
c.id,
c.category_id,
c.sub_category_id,
c.title,
c.description,
c.thumbnail,
c.intro_video_url,
c.is_active
FROM courses c
JOIN course_categories cc ON cc.id = c.category_id
WHERE lower(trim(cc.name)) = 'human language'
ORDER BY c.display_order ASC, c.id ASC
LIMIT $2::INT
OFFSET $1::INT
`
type GetHumanLanguageCoursesParams struct {
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetHumanLanguageCoursesRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
CategoryID int64 `json:"category_id"`
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
IsActive bool `json:"is_active"`
}
func (q *Queries) GetHumanLanguageCourses(ctx context.Context, arg GetHumanLanguageCoursesParams) ([]GetHumanLanguageCoursesRow, error) {
rows, err := q.db.Query(ctx, GetHumanLanguageCourses, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetHumanLanguageCoursesRow
for rows.Next() {
var i GetHumanLanguageCoursesRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.CategoryID,
&i.SubCategoryID,
&i.Title,
&i.Description,
&i.Thumbnail,
&i.IntroVideoUrl,
&i.IsActive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ReorderCourses = `-- name: ReorderCourses :exec
UPDATE courses
SET display_order = bulk.position

View File

@ -364,6 +364,171 @@ func (q *Queries) CreateSubModuleVideo(ctx context.Context, arg CreateSubModuleV
return i, err
}
const GetAllLevels = `-- name: GetAllLevels :many
SELECT
COUNT(*) OVER () AS total_count,
l.id, l.course_id, l.cefr_level, l.display_order, l.is_active, l.created_at
FROM levels l
ORDER BY l.display_order ASC, l.id ASC
LIMIT $2::INT
OFFSET $1::INT
`
type GetAllLevelsParams struct {
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetAllLevelsRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
CourseID int64 `json:"course_id"`
CefrLevel string `json:"cefr_level"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) GetAllLevels(ctx context.Context, arg GetAllLevelsParams) ([]GetAllLevelsRow, error) {
rows, err := q.db.Query(ctx, GetAllLevels, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllLevelsRow
for rows.Next() {
var i GetAllLevelsRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.CourseID,
&i.CefrLevel,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAllModules = `-- name: GetAllModules :many
SELECT
COUNT(*) OVER () AS total_count,
m.id, m.level_id, m.title, m.description, m.display_order, m.is_active, m.created_at
FROM modules m
ORDER BY m.display_order ASC, m.id ASC
LIMIT $2::INT
OFFSET $1::INT
`
type GetAllModulesParams struct {
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetAllModulesRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
LevelID int64 `json:"level_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) GetAllModules(ctx context.Context, arg GetAllModulesParams) ([]GetAllModulesRow, error) {
rows, err := q.db.Query(ctx, GetAllModules, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllModulesRow
for rows.Next() {
var i GetAllModulesRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.LevelID,
&i.Title,
&i.Description,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAllSubModules = `-- name: GetAllSubModules :many
SELECT
COUNT(*) OVER () AS total_count,
sm.id, sm.module_id, sm.title, sm.description, sm.display_order, sm.is_active, sm.created_at, sm.legacy_sub_course_id
FROM sub_modules sm
ORDER BY sm.display_order ASC, sm.id ASC
LIMIT $2::INT
OFFSET $1::INT
`
type GetAllSubModulesParams struct {
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetAllSubModulesRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
ModuleID int64 `json:"module_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
LegacySubCourseID pgtype.Int8 `json:"legacy_sub_course_id"`
}
func (q *Queries) GetAllSubModules(ctx context.Context, arg GetAllSubModulesParams) ([]GetAllSubModulesRow, error) {
rows, err := q.db.Query(ctx, GetAllSubModules, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllSubModulesRow
for rows.Next() {
var i GetAllSubModulesRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.ModuleID,
&i.Title,
&i.Description,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
&i.LegacySubCourseID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCourseSubCategories = `-- name: GetCourseSubCategories :many
SELECT
COUNT(*) OVER () AS total_count,
@ -607,6 +772,26 @@ func (q *Queries) GetHumanLanguageCourseSubCategories(ctx context.Context, arg G
return items, nil
}
const GetLevelByID = `-- name: GetLevelByID :one
SELECT id, course_id, cefr_level, display_order, is_active, created_at
FROM levels
WHERE id = $1
`
func (q *Queries) GetLevelByID(ctx context.Context, id int64) (Level, error) {
row := q.db.QueryRow(ctx, GetLevelByID, id)
var i Level
err := row.Scan(
&i.ID,
&i.CourseID,
&i.CefrLevel,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
)
return i, err
}
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
SELECT id, course_id, cefr_level, display_order, is_active, created_at
FROM levels
@ -642,6 +827,27 @@ func (q *Queries) GetLevelsByCourseID(ctx context.Context, courseID int64) ([]Le
return items, nil
}
const GetModuleByID = `-- name: GetModuleByID :one
SELECT id, level_id, title, description, display_order, is_active, created_at
FROM modules
WHERE id = $1
`
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
row := q.db.QueryRow(ctx, GetModuleByID, id)
var i Module
err := row.Scan(
&i.ID,
&i.LevelID,
&i.Title,
&i.Description,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
)
return i, err
}
const GetModulesByLevelID = `-- name: GetModulesByLevelID :many
SELECT id, level_id, title, description, display_order, is_active, created_at
FROM modules
@ -678,6 +884,28 @@ func (q *Queries) GetModulesByLevelID(ctx context.Context, levelID int64) ([]Mod
return items, nil
}
const GetSubModuleByID = `-- name: GetSubModuleByID :one
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
FROM sub_modules
WHERE id = $1
`
func (q *Queries) GetSubModuleByID(ctx context.Context, id int64) (SubModule, error) {
row := q.db.QueryRow(ctx, GetSubModuleByID, id)
var i SubModule
err := row.Scan(
&i.ID,
&i.ModuleID,
&i.Title,
&i.Description,
&i.DisplayOrder,
&i.IsActive,
&i.CreatedAt,
&i.LegacySubCourseID,
)
return i, err
}
const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one
SELECT
smp.id,
@ -798,6 +1026,62 @@ func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([
return items, nil
}
const GetSubModulePracticeByID = `-- name: GetSubModulePracticeByID :one
SELECT
smp.id,
smp.sub_module_id,
smp.title,
smp.description,
smp.thumbnail,
smp.intro_video_url,
smp.question_set_id,
smp.display_order,
smp.is_active,
qs.status,
qs.set_type,
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
FROM sub_module_practices smp
JOIN question_sets qs ON qs.id = smp.question_set_id
WHERE smp.id = $1
AND smp.is_active = TRUE
AND qs.set_type = 'PRACTICE'
`
type GetSubModulePracticeByIDRow struct {
ID int64 `json:"id"`
SubModuleID int64 `json:"sub_module_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
QuestionSetID int64 `json:"question_set_id"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
Status string `json:"status"`
SetType string `json:"set_type"`
QuestionCount int64 `json:"question_count"`
}
func (q *Queries) GetSubModulePracticeByID(ctx context.Context, id int64) (GetSubModulePracticeByIDRow, error) {
row := q.db.QueryRow(ctx, GetSubModulePracticeByID, id)
var i GetSubModulePracticeByIDRow
err := row.Scan(
&i.ID,
&i.SubModuleID,
&i.Title,
&i.Description,
&i.Thumbnail,
&i.IntroVideoUrl,
&i.QuestionSetID,
&i.DisplayOrder,
&i.IsActive,
&i.Status,
&i.SetType,
&i.QuestionCount,
)
return i, err
}
const GetSubModulePractices = `-- name: GetSubModulePractices :many
SELECT
smp.id,

View File

@ -269,6 +269,501 @@ func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error {
})
}
// ListAllCourses godoc
// @Summary List all courses
// @Description Returns all courses with pagination
// @Tags course-management
// @Produce json
// @Param offset query int false "Offset"
// @Param limit query int false "Limit"
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses [get]
func (h *Handler) ListAllCourses(c *fiber.Ctx) error {
offset := int32(c.QueryInt("offset", 0))
if offset < 0 {
offset = 0
}
limit := int32(c.QueryInt("limit", 10000))
if limit <= 0 {
limit = 10000
}
rows, err := h.analyticsDB.GetAllCourses(c.Context(), dbgen.GetAllCoursesParams{
Offset: pgtype.Int4{Int32: offset, Valid: true},
Limit: pgtype.Int4{Int32: limit, Valid: true},
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()})
}
total := 0
if len(rows) > 0 {
total = int(rows[0].TotalCount)
}
return c.JSON(domain.Response{
Message: "Courses retrieved successfully",
Data: map[string]interface{}{
"courses": rows,
"total_count": total,
},
})
}
// ListHumanLanguageCourses godoc
// @Summary List Human Language courses
// @Description Returns all courses under Human Language category
// @Tags course-management
// @Produce json
// @Param offset query int false "Offset"
// @Param limit query int false "Limit"
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/human-language/courses [get]
func (h *Handler) ListHumanLanguageCourses(c *fiber.Ctx) error {
offset := int32(c.QueryInt("offset", 0))
if offset < 0 {
offset = 0
}
limit := int32(c.QueryInt("limit", 10000))
if limit <= 0 {
limit = 10000
}
rows, err := h.analyticsDB.GetHumanLanguageCourses(c.Context(), dbgen.GetHumanLanguageCoursesParams{
Offset: pgtype.Int4{Int32: offset, Valid: true},
Limit: pgtype.Int4{Int32: limit, Valid: true},
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load Human Language courses", Error: err.Error()})
}
total := 0
if len(rows) > 0 {
total = int(rows[0].TotalCount)
}
return c.JSON(domain.Response{
Message: "Human Language courses retrieved successfully",
Data: map[string]interface{}{
"courses": rows,
"total_count": total,
},
})
}
// ListCoursesBySubCategory godoc
// @Summary List courses by sub-category
// @Description Returns courses for one sub-category
// @Tags course-management
// @Produce json
// @Param subCategoryId path int true "Sub-category ID"
// @Param offset query int false "Offset"
// @Param limit query int false "Limit"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-categories/{subCategoryId}/courses [get]
func (h *Handler) ListCoursesBySubCategory(c *fiber.Ctx) error {
subCategoryID, err := strconv.ParseInt(c.Params("subCategoryId"), 10, 64)
if err != nil || subCategoryID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-category ID", Error: "subCategoryId must be a positive integer"})
}
offset := int32(c.QueryInt("offset", 0))
if offset < 0 {
offset = 0
}
limit := int32(c.QueryInt("limit", 10000))
if limit <= 0 {
limit = 10000
}
rows, err := h.analyticsDB.GetCoursesBySubCategory(c.Context(), dbgen.GetCoursesBySubCategoryParams{
SubCategoryID: pgtype.Int8{Int64: subCategoryID, Valid: true},
Offset: pgtype.Int4{Int32: offset, Valid: true},
Limit: pgtype.Int4{Int32: limit, Valid: true},
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()})
}
total := 0
if len(rows) > 0 {
total = int(rows[0].TotalCount)
}
return c.JSON(domain.Response{
Message: "Courses retrieved successfully",
Data: map[string]interface{}{
"courses": rows,
"total_count": total,
},
})
}
// GetCourseByID godoc
// @Summary Get course detail
// @Description Returns one course by ID
// @Tags course-management
// @Produce json
// @Param courseId path int true "Course ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses/{courseId} [get]
func (h *Handler) GetCourseByID(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil || courseID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid course ID",
Error: "courseId must be a positive integer",
})
}
course, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Course not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Course retrieved successfully",
Data: course,
})
}
// ListAllLevels godoc
// @Summary List all levels
// @Description Returns all levels with pagination
// @Tags course-management
// @Produce json
// @Param offset query int false "Offset"
// @Param limit query int false "Limit"
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/levels [get]
func (h *Handler) ListAllLevels(c *fiber.Ctx) error {
offset := int32(c.QueryInt("offset", 0))
if offset < 0 {
offset = 0
}
limit := int32(c.QueryInt("limit", 10000))
if limit <= 0 {
limit = 10000
}
rows, err := h.analyticsDB.GetAllLevels(c.Context(), dbgen.GetAllLevelsParams{
Offset: pgtype.Int4{Int32: offset, Valid: true},
Limit: pgtype.Int4{Int32: limit, Valid: true},
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load levels", Error: err.Error()})
}
total := 0
if len(rows) > 0 {
total = int(rows[0].TotalCount)
}
return c.JSON(domain.Response{
Message: "Levels retrieved successfully",
Data: map[string]interface{}{
"levels": rows,
"total_count": total,
},
})
}
// ListLevelsByCourse godoc
// @Summary List levels by course
// @Description Returns all active levels for one course
// @Tags course-management
// @Produce json
// @Param courseId path int true "Course ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/courses/{courseId}/levels [get]
func (h *Handler) ListLevelsByCourse(c *fiber.Ctx) error {
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
if err != nil || courseID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid course ID",
Error: "courseId must be a positive integer",
})
}
rows, err := h.analyticsDB.GetLevelsByCourseID(c.Context(), courseID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load levels", Error: err.Error()})
}
return c.JSON(domain.Response{
Message: "Levels retrieved successfully",
Data: map[string]interface{}{
"levels": rows,
"total_count": len(rows),
},
})
}
// GetLevelByID godoc
// @Summary Get level detail
// @Description Returns one level by ID
// @Tags course-management
// @Produce json
// @Param levelId path int true "Level ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/levels/{levelId} [get]
func (h *Handler) GetLevelByID(c *fiber.Ctx) error {
levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64)
if err != nil || levelID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid level ID",
Error: "levelId must be a positive integer",
})
}
level, err := h.analyticsDB.GetLevelByID(c.Context(), levelID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Level not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Level retrieved successfully",
Data: level,
})
}
// ListAllModules godoc
// @Summary List all modules
// @Description Returns all modules with pagination
// @Tags course-management
// @Produce json
// @Param offset query int false "Offset"
// @Param limit query int false "Limit"
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/modules [get]
func (h *Handler) ListAllModules(c *fiber.Ctx) error {
offset := int32(c.QueryInt("offset", 0))
if offset < 0 {
offset = 0
}
limit := int32(c.QueryInt("limit", 10000))
if limit <= 0 {
limit = 10000
}
rows, err := h.analyticsDB.GetAllModules(c.Context(), dbgen.GetAllModulesParams{
Offset: pgtype.Int4{Int32: offset, Valid: true},
Limit: pgtype.Int4{Int32: limit, Valid: true},
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load modules", Error: err.Error()})
}
total := 0
if len(rows) > 0 {
total = int(rows[0].TotalCount)
}
return c.JSON(domain.Response{
Message: "Modules retrieved successfully",
Data: map[string]interface{}{
"modules": rows,
"total_count": total,
},
})
}
// ListModulesByLevel godoc
// @Summary List modules by level
// @Description Returns all active modules for one level
// @Tags course-management
// @Produce json
// @Param levelId path int true "Level ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/levels/{levelId}/modules [get]
func (h *Handler) ListModulesByLevel(c *fiber.Ctx) error {
levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64)
if err != nil || levelID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid level ID",
Error: "levelId must be a positive integer",
})
}
rows, err := h.analyticsDB.GetModulesByLevelID(c.Context(), levelID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load modules", Error: err.Error()})
}
return c.JSON(domain.Response{
Message: "Modules retrieved successfully",
Data: map[string]interface{}{
"modules": rows,
"total_count": len(rows),
},
})
}
// GetModuleByID godoc
// @Summary Get module detail
// @Description Returns one module by ID
// @Tags course-management
// @Produce json
// @Param moduleId path int true "Module ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/modules/{moduleId} [get]
func (h *Handler) GetModuleByID(c *fiber.Ctx) error {
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
if err != nil || moduleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid module ID",
Error: "moduleId must be a positive integer",
})
}
mod, err := h.analyticsDB.GetModuleByID(c.Context(), moduleID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Module not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Module retrieved successfully",
Data: mod,
})
}
// ListAllSubModules godoc
// @Summary List all sub-modules
// @Description Returns all sub-modules with pagination
// @Tags course-management
// @Produce json
// @Param offset query int false "Offset"
// @Param limit query int false "Limit"
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-modules [get]
func (h *Handler) ListAllSubModules(c *fiber.Ctx) error {
offset := int32(c.QueryInt("offset", 0))
if offset < 0 {
offset = 0
}
limit := int32(c.QueryInt("limit", 10000))
if limit <= 0 {
limit = 10000
}
rows, err := h.analyticsDB.GetAllSubModules(c.Context(), dbgen.GetAllSubModulesParams{
Offset: pgtype.Int4{Int32: offset, Valid: true},
Limit: pgtype.Int4{Int32: limit, Valid: true},
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-modules", Error: err.Error()})
}
total := 0
if len(rows) > 0 {
total = int(rows[0].TotalCount)
}
return c.JSON(domain.Response{
Message: "Sub-modules retrieved successfully",
Data: map[string]interface{}{
"sub_modules": rows,
"total_count": total,
},
})
}
// ListSubModulesByModule godoc
// @Summary List sub-modules by module
// @Description Returns all active sub-modules for one module
// @Tags course-management
// @Produce json
// @Param moduleId path int true "Module ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/modules/{moduleId}/sub-modules [get]
func (h *Handler) ListSubModulesByModule(c *fiber.Ctx) error {
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
if err != nil || moduleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid module ID",
Error: "moduleId must be a positive integer",
})
}
rows, err := h.analyticsDB.GetSubModulesByModuleID(c.Context(), moduleID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-modules", Error: err.Error()})
}
return c.JSON(domain.Response{
Message: "Sub-modules retrieved successfully",
Data: map[string]interface{}{
"sub_modules": rows,
"total_count": len(rows),
},
})
}
// GetSubModuleByID godoc
// @Summary Get sub-module detail
// @Description Returns one sub-module by ID
// @Tags course-management
// @Produce json
// @Param subModuleId path int true "Sub-module ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-modules/{subModuleId} [get]
func (h *Handler) GetSubModuleByID(c *fiber.Ctx) error {
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
if err != nil || subModuleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-module ID",
Error: "subModuleId must be a positive integer",
})
}
subModule, err := h.analyticsDB.GetSubModuleByID(c.Context(), subModuleID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Sub-module not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Sub-module retrieved successfully",
Data: subModule,
})
}
// ListCourseSubCategories godoc
// @Summary List course sub-categories
// @Description Returns all active course sub-categories
@ -1383,6 +1878,78 @@ func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
}
// GetSubModulePractices godoc
// @Summary Get practices under sub-module
// @Description Returns all active practices attached to a sub-module
// @Tags course-management
// @Accept json
// @Produce json
// @Param subModuleId path int true "Sub-module ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/sub-modules/{subModuleId}/practices [get]
func (h *Handler) GetSubModulePractices(c *fiber.Ctx) error {
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
if err != nil || subModuleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid sub-module ID",
Error: "subModuleId must be a positive integer",
})
}
practices, err := h.analyticsDB.GetSubModulePractices(c.Context(), subModuleID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load sub-module practices",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Sub-module practices retrieved successfully",
Data: map[string]interface{}{
"practices": practices,
"total_count": len(practices),
},
})
}
// GetSubModulePracticeByID godoc
// @Summary Get practice detail
// @Description Returns one active practice by practice ID
// @Tags course-management
// @Accept json
// @Produce json
// @Param practiceId path int true "Practice ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/course-management/practices/{practiceId} [get]
func (h *Handler) GetSubModulePracticeByID(c *fiber.Ctx) error {
practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64)
if err != nil || practiceID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid practice ID",
Error: "practiceId must be a positive integer",
})
}
practice, err := h.analyticsDB.GetSubModulePracticeByID(c.Context(), practiceID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Practice not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Practice retrieved successfully",
Data: practice,
})
}
func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error {
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
if err != nil || subModuleID <= 0 {

View File

@ -80,7 +80,19 @@ func (a *App) initAppRoutes() {
// Unified Course Management (single hierarchy model)
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories)
groupV1.Get("/course-management/categories/:categoryId/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesByCategory)
groupV1.Get("/course-management/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllCourses)
groupV1.Get("/course-management/human-language/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListHumanLanguageCourses)
groupV1.Get("/course-management/sub-categories/:subCategoryId/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesBySubCategory)
groupV1.Get("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseByID)
groupV1.Get("/course-management/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllLevels)
groupV1.Get("/course-management/courses/:courseId/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListLevelsByCourse)
groupV1.Get("/course-management/levels/:levelId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetLevelByID)
groupV1.Get("/course-management/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllModules)
groupV1.Get("/course-management/levels/:levelId/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListModulesByLevel)
groupV1.Get("/course-management/modules/:moduleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetModuleByID)
groupV1.Get("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllSubModules)
groupV1.Get("/course-management/modules/:moduleId/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListSubModulesByModule)
groupV1.Get("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleByID)
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
groupV1.Delete("/course-management/categories/:categoryId", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseCategory)
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
@ -101,7 +113,6 @@ func (a *App) initAppRoutes() {
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
groupV1.Put("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModule)
groupV1.Delete("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubModule)
groupV1.Get("/course-management/sub-modules/:subModuleId/videos", a.authMiddleware, a.RequirePermission("videos.list"), h.GetSubModuleVideos)
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo)
groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
@ -109,6 +120,8 @@ func (a *App) initAppRoutes() {
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleLessonByID)
groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateSubModuleLesson)
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
groupV1.Get("/course-management/sub-modules/:subModuleId/practices", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModulePractices)
groupV1.Get("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModulePracticeByID)
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)
groupV1.Delete("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeletePractice)