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 WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED'; 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 -- name: GetQuestionSetsContainingQuestion :many
SELECT qs.* SELECT qs.*
FROM question_sets qs FROM question_sets qs

View File

@ -362,6 +362,43 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
return items, nil 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 const RemoveQuestionFromSet = `-- name: RemoveQuestionFromSet :exec
DELETE FROM question_set_items DELETE FROM question_set_items
WHERE set_id = $1 AND question_id = $2 WHERE set_id = $1 AND question_id = $2

View File

@ -120,6 +120,19 @@ type QuestionSetItem struct {
CreatedAt time.Time 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 { type QuestionSetItemWithQuestion struct {
QuestionSetItem QuestionSetItem
QuestionText string QuestionText string

View File

@ -58,6 +58,7 @@ type QuestionStore interface {
RemoveQuestionFromSet(ctx context.Context, setID, questionID int64) error RemoveQuestionFromSet(ctx context.Context, setID, questionID int64) error
UpdateQuestionOrder(ctx context.Context, setID, questionID int64, displayOrder int32) error UpdateQuestionOrder(ctx context.Context, setID, questionID int64, displayOrder int32) error
CountQuestionsInSet(ctx context.Context, setID int64) (int64, 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) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error)
// User Personas in Question Sets // 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) 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) { func (s *Store) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
sets, err := s.queries.GetQuestionSetsContainingQuestion(ctx, questionID) sets, err := s.queries.GetQuestionSetsContainingQuestion(ctx, questionID)
if err != nil { 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) 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) { func (s *Service) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error) {
return s.questionStore.GetQuestionSetsContainingQuestion(ctx, questionID) 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 // GetQuestionSetItems godoc
// @Summary Get questions in set // @Summary Get questions in set
// @Description Returns all questions in a question set with details // @Description Returns all questions in a question set with details

View File

@ -243,6 +243,7 @@ func (a *App) initAppRoutes() {
// Question Set Items // Question Set Items
groupV1.Post("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.add"), h.AddQuestionToSet) 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("/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.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) groupV1.Delete("/question-sets/:setId/questions/:questionId", a.authMiddleware, a.RequirePermission("question_set_items.remove"), h.RemoveQuestionFromSet)