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
|
||||
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
|
||||
SELECT
|
||||
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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"get": {
|
||||
"description": "Returns all courses with pagination",
|
||||
|
|
|
|||
|
|
@ -2742,6 +2742,45 @@ paths:
|
|||
summary: List courses by category
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
description: Returns all courses with pagination
|
||||
|
|
|
|||
|
|
@ -775,6 +775,74 @@ func (q *Queries) GetCourseSubCategories(ctx context.Context, arg GetCourseSubCa
|
|||
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
|
||||
SELECT
|
||||
cc.id AS category_id,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package handlers
|
|||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"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
|
||||
// @Summary List Human Language sub-categories
|
||||
// @Description Returns active sub-categories under Human Language category
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ 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/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/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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user