Add GET question-sets question-types endpoint for practice sets.

Returns distinct question_type values with per-type counts so clients can resolve types from question_set_id without loading full questions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-06-02 06:35:46 -07:00
parent 256183ae64
commit a75700ffaa
8 changed files with 140 additions and 0 deletions

View File

@ -102,6 +102,17 @@ JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED';
-- name: GetQuestionTypeCountsInSet :many
SELECT
q.question_type,
COUNT(*)::bigint AS question_count
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED'
GROUP BY q.question_type
ORDER BY q.question_type;
-- name: GetQuestionSetsContainingQuestion :many
SELECT qs.*
FROM question_sets qs

View File

@ -362,6 +362,43 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
return items, nil
}
const GetQuestionTypeCountsInSet = `-- name: GetQuestionTypeCountsInSet :many
SELECT
q.question_type,
COUNT(*)::bigint AS question_count
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED'
GROUP BY q.question_type
ORDER BY q.question_type
`
type GetQuestionTypeCountsInSetRow struct {
QuestionType string `json:"question_type"`
QuestionCount int64 `json:"question_count"`
}
func (q *Queries) GetQuestionTypeCountsInSet(ctx context.Context, setID int64) ([]GetQuestionTypeCountsInSetRow, error) {
rows, err := q.db.Query(ctx, GetQuestionTypeCountsInSet, setID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetQuestionTypeCountsInSetRow
for rows.Next() {
var i GetQuestionTypeCountsInSetRow
if err := rows.Scan(&i.QuestionType, &i.QuestionCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const RemoveQuestionFromSet = `-- name: RemoveQuestionFromSet :exec
DELETE FROM question_set_items
WHERE set_id = $1 AND question_id = $2

View File

@ -120,6 +120,19 @@ type QuestionSetItem struct {
CreatedAt time.Time
}
// QuestionSetQuestionTypeCount is one question_type present in a set with how many questions use it.
type QuestionSetQuestionTypeCount struct {
QuestionType string `json:"question_type"`
Count int64 `json:"count"`
}
// QuestionSetQuestionTypesSummary summarizes distinct question types in a question set (e.g. linked practice).
type QuestionSetQuestionTypesSummary struct {
QuestionSetID int64 `json:"question_set_id"`
QuestionTypes []QuestionSetQuestionTypeCount `json:"question_types"`
TotalQuestions int64 `json:"total_questions"`
}
type QuestionSetItemWithQuestion struct {
QuestionSetItem
QuestionText string

View File

@ -58,6 +58,7 @@ type QuestionStore interface {
RemoveQuestionFromSet(ctx context.Context, setID, questionID int64) error
UpdateQuestionOrder(ctx context.Context, setID, questionID int64, displayOrder int32) error
CountQuestionsInSet(ctx context.Context, setID int64) (int64, error)
GetQuestionTypeCountsInSet(ctx context.Context, setID int64) ([]domain.QuestionSetQuestionTypeCount, error)
GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error)
// User Personas in Question Sets

View File

@ -1226,6 +1226,21 @@ func (s *Store) CountQuestionsInSet(ctx context.Context, setID int64) (int64, er
return s.queries.CountQuestionsInSet(ctx, setID)
}
func (s *Store) GetQuestionTypeCountsInSet(ctx context.Context, setID int64) ([]domain.QuestionSetQuestionTypeCount, error) {
rows, err := s.queries.GetQuestionTypeCountsInSet(ctx, setID)
if err != nil {
return nil, err
}
result := make([]domain.QuestionSetQuestionTypeCount, len(rows))
for i, r := range rows {
result[i] = domain.QuestionSetQuestionTypeCount{
QuestionType: r.QuestionType,
Count: r.QuestionCount,
}
}
return result, nil
}
func (s *Store) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
sets, err := s.queries.GetQuestionSetsContainingQuestion(ctx, questionID)
if err != nil {

View File

@ -192,6 +192,25 @@ func (s *Service) CountQuestionsInSet(ctx context.Context, setID int64) (int64,
return s.questionStore.CountQuestionsInSet(ctx, setID)
}
func (s *Service) GetQuestionTypesInSet(ctx context.Context, setID int64) (domain.QuestionSetQuestionTypesSummary, error) {
counts, err := s.questionStore.GetQuestionTypeCountsInSet(ctx, setID)
if err != nil {
return domain.QuestionSetQuestionTypesSummary{}, err
}
var total int64
for _, c := range counts {
total += c.Count
}
if counts == nil {
counts = []domain.QuestionSetQuestionTypeCount{}
}
return domain.QuestionSetQuestionTypesSummary{
QuestionSetID: setID,
QuestionTypes: counts,
TotalQuestions: total,
}, nil
}
func (s *Service) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
return s.questionStore.GetQuestionSetsContainingQuestion(ctx, questionID)
}

View File

@ -1322,6 +1322,49 @@ func (h *Handler) AddQuestionToSet(c *fiber.Ctx) error {
})
}
// GetQuestionTypesInSet godoc
// @Summary List question types in a question set
// @Description Returns distinct question_type values (with counts) for non-archived questions linked to the set. Use the question_set_id from a practice to see which types were used to build it.
// @Tags question-set-items
// @Produce json
// @Param setId path int true "Question Set ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/question-sets/{setId}/question-types [get]
func (h *Handler) GetQuestionTypesInSet(c *fiber.Ctx) error {
setID, err := strconv.ParseInt(c.Params("setId"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid set ID",
Error: err.Error(),
})
}
if _, err := h.questionsSvc.GetQuestionSetByID(c.Context(), setID); err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Question set not found",
Error: err.Error(),
})
}
summary, err := h.questionsSvc.GetQuestionTypesInSet(c.Context(), setID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get question types in set",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Question types retrieved successfully",
Success: true,
StatusCode: fiber.StatusOK,
Data: summary,
})
}
// GetQuestionSetItems godoc
// @Summary Get questions in set
// @Description Returns all questions in a question set with details

View File

@ -243,6 +243,7 @@ func (a *App) initAppRoutes() {
// Question Set Items
groupV1.Post("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.add"), h.AddQuestionToSet)
groupV1.Get("/question-sets/:setId/question-types", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionTypesInSet)
groupV1.Get("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionsInSet)
groupV1.Get("/practices/:practiceId/questions", a.authMiddleware, a.RequireSubscriptionCategory(domain.SubscriptionCategoryLearnEnglish), a.RequirePermission("question_set_items.list"), h.GetQuestionsByPractice)
groupV1.Delete("/question-sets/:setId/questions/:questionId", a.authMiddleware, a.RequirePermission("question_set_items.remove"), h.RemoveQuestionFromSet)