feat: list sub-categories by course category ID
- GET /api/v1/course-management/categories/:categoryId/sub-categories - SQL GetCourseSubCategoriesByCategoryID; swagger refresh Made-with: Cursor
This commit is contained in:
parent
de95c4d0d2
commit
72d1a0c3ed
|
|
@ -246,6 +246,25 @@ ORDER BY csc.display_order ASC, csc.id ASC
|
||||||
LIMIT sqlc.narg('limit')::INT
|
LIMIT sqlc.narg('limit')::INT
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
-- name: GetCourseSubCategoriesByCategoryID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
csc.id,
|
||||||
|
csc.category_id,
|
||||||
|
cc.name AS category_name,
|
||||||
|
csc.name,
|
||||||
|
csc.description,
|
||||||
|
csc.display_order,
|
||||||
|
csc.is_active,
|
||||||
|
csc.created_at
|
||||||
|
FROM course_sub_categories csc
|
||||||
|
JOIN course_categories cc ON cc.id = csc.category_id
|
||||||
|
WHERE csc.category_id = $1
|
||||||
|
AND csc.is_active = TRUE
|
||||||
|
ORDER BY csc.display_order ASC, csc.id ASC
|
||||||
|
LIMIT sqlc.narg('limit')::INT
|
||||||
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
-- name: GetHumanLanguageCourseSubCategories :many
|
-- name: GetHumanLanguageCourseSubCategories :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
|
|
|
||||||
59
docs/docs.go
59
docs/docs.go
|
|
@ -979,6 +979,65 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/course-management/categories/{categoryId}/sub-categories": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns active sub-categories for the given category ID",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "List sub-categories for a course category",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Category ID",
|
||||||
|
"name": "categoryId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/course-management/courses": {
|
"/api/v1/course-management/courses": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns all courses with pagination",
|
"description": "Returns all courses with pagination",
|
||||||
|
|
|
||||||
|
|
@ -971,6 +971,65 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/course-management/categories/{categoryId}/sub-categories": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns active sub-categories for the given category ID",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "List sub-categories for a course category",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Category ID",
|
||||||
|
"name": "categoryId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/course-management/courses": {
|
"/api/v1/course-management/courses": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns all courses with pagination",
|
"description": "Returns all courses with pagination",
|
||||||
|
|
|
||||||
|
|
@ -2742,6 +2742,45 @@ paths:
|
||||||
summary: List courses by category
|
summary: List courses by category
|
||||||
tags:
|
tags:
|
||||||
- course-management
|
- course-management
|
||||||
|
/api/v1/course-management/categories/{categoryId}/sub-categories:
|
||||||
|
get:
|
||||||
|
description: Returns active sub-categories for the given category ID
|
||||||
|
parameters:
|
||||||
|
- description: Category ID
|
||||||
|
in: path
|
||||||
|
name: categoryId
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Offset
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
- description: Limit
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: List sub-categories for a course category
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
/api/v1/course-management/courses:
|
/api/v1/course-management/courses:
|
||||||
get:
|
get:
|
||||||
description: Returns all courses with pagination
|
description: Returns all courses with pagination
|
||||||
|
|
|
||||||
|
|
@ -775,6 +775,74 @@ func (q *Queries) GetCourseSubCategories(ctx context.Context, arg GetCourseSubCa
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetCourseSubCategoriesByCategoryID = `-- name: GetCourseSubCategoriesByCategoryID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
csc.id,
|
||||||
|
csc.category_id,
|
||||||
|
cc.name AS category_name,
|
||||||
|
csc.name,
|
||||||
|
csc.description,
|
||||||
|
csc.display_order,
|
||||||
|
csc.is_active,
|
||||||
|
csc.created_at
|
||||||
|
FROM course_sub_categories csc
|
||||||
|
JOIN course_categories cc ON cc.id = csc.category_id
|
||||||
|
WHERE csc.category_id = $1
|
||||||
|
AND csc.is_active = TRUE
|
||||||
|
ORDER BY csc.display_order ASC, csc.id ASC
|
||||||
|
LIMIT $3::INT
|
||||||
|
OFFSET $2::INT
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetCourseSubCategoriesByCategoryIDParams struct {
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCourseSubCategoriesByCategoryIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetCourseSubCategoriesByCategoryID(ctx context.Context, arg GetCourseSubCategoriesByCategoryIDParams) ([]GetCourseSubCategoriesByCategoryIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetCourseSubCategoriesByCategoryID, arg.CategoryID, arg.Offset, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetCourseSubCategoriesByCategoryIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetCourseSubCategoriesByCategoryIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.CategoryName,
|
||||||
|
&i.Name,
|
||||||
|
&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 GetCoursesWithHierarchy = `-- name: GetCoursesWithHierarchy :many
|
const GetCoursesWithHierarchy = `-- name: GetCoursesWithHierarchy :many
|
||||||
SELECT
|
SELECT
|
||||||
cc.id AS category_id,
|
cc.id AS category_id,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ package handlers
|
||||||
import (
|
import (
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -908,6 +910,75 @@ func (h *Handler) ListCourseSubCategories(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListCourseSubCategoriesByCategory godoc
|
||||||
|
// @Summary List sub-categories for a course category
|
||||||
|
// @Description Returns active sub-categories for the given category ID
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param categoryId path int true "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 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/categories/{categoryId}/sub-categories [get]
|
||||||
|
func (h *Handler) ListCourseSubCategoriesByCategory(c *fiber.Ctx) error {
|
||||||
|
categoryID, err := strconv.ParseInt(c.Params("categoryId"), 10, 64)
|
||||||
|
if err != nil || categoryID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid category ID",
|
||||||
|
Error: "categoryId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if _, err := h.analyticsDB.GetCourseCategoryByID(c.Context(), categoryID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Category not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to load category",
|
||||||
|
Error: err.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.GetCourseSubCategoriesByCategoryID(c.Context(), dbgen.GetCourseSubCategoriesByCategoryIDParams{
|
||||||
|
CategoryID: categoryID,
|
||||||
|
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-categories",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(rows) > 0 {
|
||||||
|
total = int(rows[0].TotalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Sub-categories retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"sub_categories": rows,
|
||||||
|
"total_count": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ListHumanLanguageCourseSubCategories godoc
|
// ListHumanLanguageCourseSubCategories godoc
|
||||||
// @Summary List Human Language sub-categories
|
// @Summary List Human Language sub-categories
|
||||||
// @Description Returns active sub-categories under Human Language category
|
// @Description Returns active sub-categories under Human Language category
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
// Unified Course Management (single hierarchy model)
|
// 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", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories)
|
||||||
|
groupV1.Get("/course-management/categories/:categoryId/sub-categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseSubCategoriesByCategory)
|
||||||
groupV1.Get("/course-management/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllCourses)
|
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/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/sub-categories/:subCategoryId/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesBySubCategory)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user