Yimaru-BackEnd/internal/repository/lms_lessons.go
Yared Yemane bd1767d2a6 Add LMS lesson draft and publish visibility.
Migration 000062 adds lessons.publish_status (DRAFT default for new rows; existing rows published). Editors see all lessons; learners see published-only lists and GET by id. Sequential prerequisites and completion counts ignore drafts. Course lesson_count counts published lessons only. Swagger documents publish_status on create/update bodies.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 02:16:42 -07:00

213 lines
5.8 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 lessonToDomain(l dbgen.Lesson) domain.Lesson {
out := domain.Lesson{
ID: l.ID,
ModuleID: l.ModuleID,
Title: l.Title,
PublishStatus: domain.LessonPublishStatusFromDB(l.PublishStatus),
}
out.VideoURL = fromPgText(l.VideoUrl)
out.Thumbnail = fromPgText(l.Thumbnail)
out.Description = fromPgText(l.Description)
out.CreatedAt = l.CreatedAt.Time
if l.UpdatedAt.Valid {
t := l.UpdatedAt.Time
out.UpdatedAt = &t
}
out.SortOrder = int(l.SortOrder)
return out
}
func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
pub := string(domain.LessonPublishStatusFromCreateInput(input.PublishStatus))
if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx)
if err != nil {
return domain.Lesson{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx,
`UPDATE lessons SET sort_order = sort_order + 1 WHERE module_id = $1 AND sort_order >= $2`,
moduleID, target,
); err != nil {
return domain.Lesson{}, err
}
l, err := q.CreateLesson(ctx, dbgen.CreateLessonParams{
ModuleID: moduleID,
Title: input.Title,
VideoUrl: toPgText(input.VideoURL),
Thumbnail: toPgText(input.Thumbnail),
Description: toPgText(input.Description),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.Lesson{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.Lesson{}, err
}
return lessonToDomain(l), nil
}
l, err := s.queries.CreateLesson(ctx, dbgen.CreateLessonParams{
ModuleID: moduleID,
Title: input.Title,
VideoUrl: toPgText(input.VideoURL),
Thumbnail: toPgText(input.Thumbnail),
Description: toPgText(input.Description),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
})
if err != nil {
return domain.Lesson{}, err
}
return lessonToDomain(l), nil
}
func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error) {
l, err := s.queries.GetLessonByID(ctx, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Lesson{}, pgx.ErrNoRows
}
return domain.Lesson{}, err
}
out := lessonToDomain(dbgen.Lesson{
ID: l.ID,
ModuleID: l.ModuleID,
Title: l.Title,
VideoUrl: l.VideoUrl,
Thumbnail: l.Thumbnail,
Description: l.Description,
SortOrder: l.SortOrder,
PublishStatus: l.PublishStatus,
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
})
out.HasPractice = l.HasPractice
return out, nil
}
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Lesson, int64, error) {
rows, err := s.queries.ListLessonsByModuleID(ctx, dbgen.ListLessonsByModuleIDParams{
ModuleID: moduleID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
}
if len(rows) == 0 {
return []domain.Lesson{}, 0, nil
}
var total int64
out := make([]domain.Lesson, 0, len(rows))
for i, r := range rows {
if i == 0 {
total = r.TotalCount
}
lesson := lessonToDomain(dbgen.Lesson{
ID: r.ID,
ModuleID: r.ModuleID,
Title: r.Title,
VideoUrl: r.VideoUrl,
Thumbnail: r.Thumbnail,
Description: r.Description,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
})
lesson.HasPractice = r.HasPractice
out = append(out, lesson)
}
return out, total, nil
}
func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
sortParam := optionalInt4Update(input.SortOrder)
pubParam := optionalPublishStatusUpdate(input.PublishStatus)
var titleText pgtype.Text
if input.Title != nil {
titleText = pgtype.Text{String: *input.Title, Valid: true}
} else {
titleText = pgtype.Text{Valid: false}
}
if input.SortOrder != nil {
cur, err := s.GetLessonByID(ctx, id)
if err != nil {
return domain.Lesson{}, err
}
oldPos := int32(cur.SortOrder)
newPos := int32(*input.SortOrder)
if oldPos != newPos {
q, tx, err := s.BeginTx(ctx)
if err != nil {
return domain.Lesson{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
if err := repositionLessonSortOrder(ctx, tx, cur.ModuleID, id, oldPos, newPos); err != nil {
return domain.Lesson{}, err
}
l, err := q.UpdateLesson(ctx, dbgen.UpdateLessonParams{
ID: id,
Title: titleText,
VideoUrl: optionalTextUpdate(input.VideoURL),
Thumbnail: optionalTextUpdate(input.Thumbnail),
Description: optionalTextUpdate(input.Description),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pubParam,
})
if err != nil {
return domain.Lesson{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.Lesson{}, err
}
out := lessonToDomain(l)
out.HasPractice = cur.HasPractice
return out, nil
}
sortParam = pgtype.Int4{Valid: false}
}
l, err := s.queries.UpdateLesson(ctx, dbgen.UpdateLessonParams{
ID: id,
Title: titleText,
VideoUrl: optionalTextUpdate(input.VideoURL),
Thumbnail: optionalTextUpdate(input.Thumbnail),
Description: optionalTextUpdate(input.Description),
SortOrder: sortParam,
PublishStatus: pubParam,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Lesson{}, pgx.ErrNoRows
}
return domain.Lesson{}, err
}
return lessonToDomain(l), nil
}
func (s *Store) DeleteLesson(ctx context.Context, id int64) error {
return s.queries.DeleteLesson(ctx, id)
}