POST /practices and exam-prep practice create accept missing or null title; persist as empty string. Refresh OpenAPI and document the behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
7.4 KiB
Go
262 lines
7.4 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
dbgen "Yimaru-Backend/gen/db"
|
|
"Yimaru-Backend/internal/domain"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func int64PtrToPg8(p *int64) pgtype.Int8 {
|
|
if p == nil {
|
|
return pgtype.Int8{Valid: false}
|
|
}
|
|
return pgtype.Int8{Int64: *p, Valid: true}
|
|
}
|
|
|
|
func fromPgInt8ID(c pgtype.Int8) *int64 {
|
|
if !c.Valid {
|
|
return nil
|
|
}
|
|
v := c.Int64
|
|
return &v
|
|
}
|
|
|
|
func optionalInt8UpdateID(val *int64) pgtype.Int8 {
|
|
if val == nil {
|
|
return pgtype.Int8{Valid: false}
|
|
}
|
|
return pgtype.Int8{Int64: *val, Valid: true}
|
|
}
|
|
|
|
func derefString(p *string) string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
return *p
|
|
}
|
|
|
|
func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice {
|
|
out := domain.Practice{
|
|
ID: p.ID,
|
|
Title: p.Title,
|
|
QuestionSetID: p.QuestionSetID,
|
|
PublishStatus: domain.PracticePublishStatusFromDB(p.PublishStatus),
|
|
}
|
|
if p.CourseID.Valid {
|
|
out.ParentKind = domain.ParentKindCourse
|
|
out.ParentID = p.CourseID.Int64
|
|
} else if p.ModuleID.Valid {
|
|
out.ParentKind = domain.ParentKindModule
|
|
out.ParentID = p.ModuleID.Int64
|
|
} else if p.LessonID.Valid {
|
|
out.ParentKind = domain.ParentKindLesson
|
|
out.ParentID = p.LessonID.Int64
|
|
}
|
|
out.StoryDescription = fromPgText(p.StoryDescription)
|
|
out.StoryImage = fromPgText(p.StoryImage)
|
|
out.QuickTips = fromPgText(p.QuickTips)
|
|
out.PersonaID = fromPgInt8ID(p.PersonaID)
|
|
out.CreatedAt = p.CreatedAt.Time
|
|
if p.UpdatedAt.Valid {
|
|
t := p.UpdatedAt.Time
|
|
out.UpdatedAt = &t
|
|
}
|
|
return out
|
|
}
|
|
|
|
func lmsFromListRow(
|
|
id, qid int64,
|
|
publishStatus string,
|
|
title string,
|
|
cid, mid, lid pgtype.Int8,
|
|
sd, si, qt pgtype.Text, pid pgtype.Int8,
|
|
ca, ua pgtype.Timestamptz,
|
|
) domain.Practice {
|
|
return lmsPracticeToDomain(dbgen.LmsPractice{
|
|
ID: id,
|
|
CourseID: cid,
|
|
ModuleID: mid,
|
|
LessonID: lid,
|
|
Title: title,
|
|
StoryDescription: sd,
|
|
StoryImage: si,
|
|
PersonaID: pid,
|
|
QuestionSetID: qid,
|
|
QuickTips: qt,
|
|
PublishStatus: publishStatus,
|
|
CreatedAt: ca,
|
|
UpdatedAt: ua,
|
|
})
|
|
}
|
|
|
|
// CreateLmsPractice sets exactly one of courseID, moduleID, lessonID (non-nil).
|
|
func (s *Store) CreateLmsPractice(
|
|
ctx context.Context,
|
|
in domain.CreatePracticeInput,
|
|
courseID, moduleID, lessonID *int64,
|
|
) (domain.Practice, error) {
|
|
ps := domain.ParsePracticePublishStatusInput(in.PublishStatus)
|
|
p, err := s.queries.CreateLmsPractice(ctx, dbgen.CreateLmsPracticeParams{
|
|
CourseID: int64PtrToPg8(courseID),
|
|
ModuleID: int64PtrToPg8(moduleID),
|
|
LessonID: int64PtrToPg8(lessonID),
|
|
Title: derefString(in.Title),
|
|
StoryDescription: toPgText(in.StoryDescription),
|
|
StoryImage: toPgText(in.StoryImage),
|
|
PersonaID: int64PtrToPg8(in.PersonaID),
|
|
QuestionSetID: in.QuestionSetID,
|
|
QuickTips: toPgText(in.QuickTips),
|
|
PublishStatus: string(ps),
|
|
})
|
|
if err != nil {
|
|
return domain.Practice{}, err
|
|
}
|
|
return lmsPracticeToDomain(p), nil
|
|
}
|
|
|
|
func (s *Store) GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error) {
|
|
p, err := s.queries.GetLmsPracticeByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Practice{}, pgx.ErrNoRows
|
|
}
|
|
return domain.Practice{}, err
|
|
}
|
|
return lmsPracticeToDomain(p), nil
|
|
}
|
|
|
|
// TryGetLmsPracticeByQuestionSetID returns false when no row exists.
|
|
func (s *Store) TryGetLmsPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.Practice, bool, error) {
|
|
p, err := s.queries.GetLmsPracticeByQuestionSetID(ctx, questionSetID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Practice{}, false, nil
|
|
}
|
|
return domain.Practice{}, false, err
|
|
}
|
|
return lmsPracticeToDomain(p), true, nil
|
|
}
|
|
|
|
func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
|
|
rows, err := s.queries.ListLmsPracticesByCourseID(ctx, dbgen.ListLmsPracticesByCourseIDParams{
|
|
CourseID: pgtype.Int8{Int64: courseID, Valid: true},
|
|
PublishedOnly: publishedOnly,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return []domain.Practice{}, 0, nil
|
|
}
|
|
var total int64
|
|
out := make([]domain.Practice, 0, len(rows))
|
|
for i, r := range rows {
|
|
if i == 0 {
|
|
total = r.TotalCount
|
|
}
|
|
out = append(out, lmsFromListRow(
|
|
r.ID, r.QuestionSetID, r.PublishStatus, r.Title,
|
|
r.CourseID, r.ModuleID, r.LessonID,
|
|
r.StoryDescription, r.StoryImage, r.QuickTips,
|
|
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
|
))
|
|
}
|
|
return out, total, nil
|
|
}
|
|
|
|
func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
|
|
rows, err := s.queries.ListLmsPracticesByModuleID(ctx, dbgen.ListLmsPracticesByModuleIDParams{
|
|
ModuleID: pgtype.Int8{Int64: moduleID, Valid: true},
|
|
PublishedOnly: publishedOnly,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return []domain.Practice{}, 0, nil
|
|
}
|
|
var total int64
|
|
out := make([]domain.Practice, 0, len(rows))
|
|
for i, r := range rows {
|
|
if i == 0 {
|
|
total = r.TotalCount
|
|
}
|
|
out = append(out, lmsFromListRow(
|
|
r.ID, r.QuestionSetID, r.PublishStatus, r.Title,
|
|
r.CourseID, r.ModuleID, r.LessonID,
|
|
r.StoryDescription, r.StoryImage, r.QuickTips,
|
|
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
|
))
|
|
}
|
|
return out, total, nil
|
|
}
|
|
|
|
func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
|
|
rows, err := s.queries.ListLmsPracticesByLessonID(ctx, dbgen.ListLmsPracticesByLessonIDParams{
|
|
LessonID: pgtype.Int8{Int64: lessonID, Valid: true},
|
|
PublishedOnly: publishedOnly,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return []domain.Practice{}, 0, nil
|
|
}
|
|
var total int64
|
|
out := make([]domain.Practice, 0, len(rows))
|
|
for i, r := range rows {
|
|
if i == 0 {
|
|
total = r.TotalCount
|
|
}
|
|
out = append(out, lmsFromListRow(
|
|
r.ID, r.QuestionSetID, r.PublishStatus, r.Title,
|
|
r.CourseID, r.ModuleID, r.LessonID,
|
|
r.StoryDescription, r.StoryImage, r.QuickTips,
|
|
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
|
))
|
|
}
|
|
return out, total, nil
|
|
}
|
|
|
|
func (s *Store) UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) {
|
|
var titleText pgtype.Text
|
|
if input.Title != nil {
|
|
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
|
} else {
|
|
titleText = pgtype.Text{Valid: false}
|
|
}
|
|
qs := optionalInt8UpdateID(input.QuestionSetID)
|
|
p, err := s.queries.UpdateLmsPractice(ctx, dbgen.UpdateLmsPracticeParams{
|
|
ID: id,
|
|
Title: titleText,
|
|
StoryDescription: optionalTextUpdate(input.StoryDescription),
|
|
StoryImage: optionalTextUpdate(input.StoryImage),
|
|
PersonaID: optionalInt8UpdateID(input.PersonaID),
|
|
QuestionSetID: qs,
|
|
QuickTips: optionalTextUpdate(input.QuickTips),
|
|
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Practice{}, pgx.ErrNoRows
|
|
}
|
|
return domain.Practice{}, err
|
|
}
|
|
return lmsPracticeToDomain(p), nil
|
|
}
|
|
|
|
func (s *Store) DeleteLmsPractice(ctx context.Context, id int64) error {
|
|
return s.queries.DeleteLmsPractice(ctx, id)
|
|
}
|