Expose has_practice booleans for LMS and pre-exam hierarchy entities, wire SQL/repository mappings, and regenerate SQLC/Swagger artifacts. Also update the Resend sender display name for outbound emails. Co-authored-by: Cursor <cursoragent@cursor.com>
204 lines
5.1 KiB
Go
204 lines
5.1 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 moduleToDomain(m dbgen.Module) domain.Module {
|
|
out := domain.Module{
|
|
ID: m.ID,
|
|
ProgramID: m.ProgramID,
|
|
CourseID: m.CourseID,
|
|
Name: m.Name,
|
|
}
|
|
out.Description = fromPgText(m.Description)
|
|
out.Icon = fromPgText(m.Icon)
|
|
out.CreatedAt = m.CreatedAt.Time
|
|
if m.UpdatedAt.Valid {
|
|
t := m.UpdatedAt.Time
|
|
out.UpdatedAt = &t
|
|
}
|
|
out.SortOrder = int(m.SortOrder)
|
|
return out
|
|
}
|
|
|
|
func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
|
|
m, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{
|
|
ProgramID: programID,
|
|
CourseID: courseID,
|
|
Name: input.Name,
|
|
Description: toPgText(input.Description),
|
|
Icon: toPgText(input.Icon),
|
|
})
|
|
if err != nil {
|
|
return domain.Module{}, err
|
|
}
|
|
return moduleToDomain(m), nil
|
|
}
|
|
|
|
func (s *Store) ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error) {
|
|
return s.queries.ListModuleIDsByCourse(ctx, courseID)
|
|
}
|
|
|
|
func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, error) {
|
|
m, err := s.queries.GetModuleByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Module{}, pgx.ErrNoRows
|
|
}
|
|
return domain.Module{}, err
|
|
}
|
|
out := moduleToDomain(dbgen.Module{
|
|
ID: m.ID,
|
|
ProgramID: m.ProgramID,
|
|
CourseID: m.CourseID,
|
|
Name: m.Name,
|
|
Description: m.Description,
|
|
Icon: m.Icon,
|
|
SortOrder: m.SortOrder,
|
|
CreatedAt: m.CreatedAt,
|
|
UpdatedAt: m.UpdatedAt,
|
|
})
|
|
out.HasPractice = m.HasPractice
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
|
rows, err := s.queries.ListModulesByProgramAndCourse(ctx, dbgen.ListModulesByProgramAndCourseParams{
|
|
ProgramID: programID,
|
|
CourseID: courseID,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return []domain.Module{}, 0, nil
|
|
}
|
|
var total int64
|
|
out := make([]domain.Module, 0, len(rows))
|
|
for i, r := range rows {
|
|
if i == 0 {
|
|
total = r.TotalCount
|
|
}
|
|
mod := moduleToDomain(dbgen.Module{
|
|
ID: r.ID,
|
|
ProgramID: r.ProgramID,
|
|
CourseID: r.CourseID,
|
|
Name: r.Name,
|
|
Description: r.Description,
|
|
Icon: r.Icon,
|
|
CreatedAt: r.CreatedAt,
|
|
UpdatedAt: r.UpdatedAt,
|
|
SortOrder: r.SortOrder,
|
|
})
|
|
mod.HasPractice = r.HasPractice
|
|
out = append(out, mod)
|
|
}
|
|
return out, total, nil
|
|
}
|
|
|
|
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
|
q, tx, err := s.BeginTx(ctx)
|
|
if err != nil {
|
|
return domain.Module{}, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
var current dbgen.Module
|
|
err = tx.QueryRow(ctx, `
|
|
SELECT id, program_id, course_id, name, description, icon, sort_order, created_at, updated_at
|
|
FROM modules
|
|
WHERE id = $1
|
|
FOR UPDATE
|
|
`, id).Scan(
|
|
¤t.ID,
|
|
¤t.ProgramID,
|
|
¤t.CourseID,
|
|
¤t.Name,
|
|
¤t.Description,
|
|
¤t.Icon,
|
|
¤t.SortOrder,
|
|
¤t.CreatedAt,
|
|
¤t.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Module{}, pgx.ErrNoRows
|
|
}
|
|
return domain.Module{}, err
|
|
}
|
|
|
|
if input.SortOrder != nil {
|
|
targetSort := int32(*input.SortOrder)
|
|
if targetSort != current.SortOrder {
|
|
var conflictID int64
|
|
err = tx.QueryRow(ctx, `
|
|
SELECT id
|
|
FROM modules
|
|
WHERE course_id = $1
|
|
AND sort_order = $2
|
|
AND id <> $3
|
|
FOR UPDATE
|
|
`, current.CourseID, targetSort, id).Scan(&conflictID)
|
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Module{}, err
|
|
}
|
|
if err == nil {
|
|
var tempSort int32
|
|
if err := tx.QueryRow(ctx, `
|
|
SELECT COALESCE(MIN(sort_order), 0) - 1
|
|
FROM modules
|
|
WHERE course_id = $1
|
|
`, current.CourseID).Scan(&tempSort); err != nil {
|
|
return domain.Module{}, err
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order = $1 WHERE id = $2`, tempSort, id); err != nil {
|
|
return domain.Module{}, err
|
|
}
|
|
if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order = $1 WHERE id = $2`, current.SortOrder, conflictID); err != nil {
|
|
return domain.Module{}, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var nameText pgtype.Text
|
|
if input.Name != nil {
|
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
|
} else {
|
|
nameText = pgtype.Text{Valid: false}
|
|
}
|
|
m, err := q.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
|
ID: id,
|
|
Name: nameText,
|
|
Description: optionalTextUpdate(input.Description),
|
|
Icon: optionalTextUpdate(input.Icon),
|
|
SortOrder: optionalInt4Update(input.SortOrder),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Module{}, pgx.ErrNoRows
|
|
}
|
|
return domain.Module{}, err
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return domain.Module{}, err
|
|
}
|
|
return moduleToDomain(m), nil
|
|
}
|
|
|
|
func (s *Store) DeleteModule(ctx context.Context, id int64) error {
|
|
return s.queries.DeleteModule(ctx, id)
|
|
}
|