Introduce plan and content categories across programs and exam-prep catalog roots, wire category-aware checkout and access checks, and keep learner gating temporarily bypassed until data migration is ready. Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
5.6 KiB
Go
214 lines
5.6 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
dbgen "Yimaru-Backend/gen/db"
|
|
"Yimaru-Backend/internal/domain"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func programToDomain(p dbgen.Program) domain.Program {
|
|
out := domain.Program{
|
|
ID: p.ID,
|
|
Name: p.Name,
|
|
Category: p.Category,
|
|
}
|
|
out.Description = fromPgText(p.Description)
|
|
out.Thumbnail = fromPgText(p.Thumbnail)
|
|
out.CreatedAt = p.CreatedAt.Time
|
|
if p.UpdatedAt.Valid {
|
|
t := p.UpdatedAt.Time
|
|
out.UpdatedAt = &t
|
|
}
|
|
out.SortOrder = int(p.SortOrder)
|
|
return out
|
|
}
|
|
|
|
func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error) {
|
|
if input.SortOrder != nil {
|
|
q, tx, err := s.BeginTx(ctx)
|
|
if err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
target := int32(*input.SortOrder)
|
|
if _, err := tx.Exec(ctx, `UPDATE programs SET sort_order = sort_order + 1 WHERE sort_order >= $1`, target); err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
p, err := q.CreateProgram(ctx, dbgen.CreateProgramParams{
|
|
Name: input.Name,
|
|
Description: toPgText(input.Description),
|
|
Category: input.Category,
|
|
Thumbnail: toPgText(input.Thumbnail),
|
|
SortOrder: pgtype.Int4{Int32: target, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
return programToDomain(p), nil
|
|
}
|
|
|
|
p, err := s.queries.CreateProgram(ctx, dbgen.CreateProgramParams{
|
|
Name: input.Name,
|
|
Description: toPgText(input.Description),
|
|
Category: input.Category,
|
|
Thumbnail: toPgText(input.Thumbnail),
|
|
SortOrder: pgtype.Int4{Valid: false},
|
|
})
|
|
if err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
return programToDomain(p), nil
|
|
}
|
|
|
|
func (s *Store) ListAllProgramIDs(ctx context.Context) ([]int64, error) {
|
|
return s.queries.ListAllProgramIDs(ctx)
|
|
}
|
|
|
|
func (s *Store) GetProgramByID(ctx context.Context, id int64) (domain.Program, error) {
|
|
p, err := s.queries.GetProgramByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Program{}, pgx.ErrNoRows
|
|
}
|
|
return domain.Program{}, err
|
|
}
|
|
return programToDomain(p), nil
|
|
}
|
|
|
|
func (s *Store) ListPrograms(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error) {
|
|
rows, err := s.queries.ListPrograms(ctx, dbgen.ListProgramsParams{
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return []domain.Program{}, 0, nil
|
|
}
|
|
var total int64
|
|
out := make([]domain.Program, 0, len(rows))
|
|
for i, r := range rows {
|
|
if i == 0 {
|
|
total = r.TotalCount
|
|
}
|
|
out = append(out, programToDomain(dbgen.Program{
|
|
ID: r.ID,
|
|
Name: r.Name,
|
|
Description: r.Description,
|
|
Category: r.Category,
|
|
Thumbnail: r.Thumbnail,
|
|
CreatedAt: r.CreatedAt,
|
|
UpdatedAt: r.UpdatedAt,
|
|
SortOrder: r.SortOrder,
|
|
}))
|
|
}
|
|
return out, total, nil
|
|
}
|
|
|
|
func optionalTextUpdate(val *string) pgtype.Text {
|
|
if val == nil {
|
|
return pgtype.Text{Valid: false}
|
|
}
|
|
return pgtype.Text{String: *val, Valid: true}
|
|
}
|
|
|
|
func optionalPublishStatusUpdate(val *string) pgtype.Text {
|
|
if val == nil {
|
|
return pgtype.Text{Valid: false}
|
|
}
|
|
s := strings.TrimSpace(strings.ToUpper(*val))
|
|
switch s {
|
|
case string(domain.PracticePublishDraft), string(domain.PracticePublishPublished):
|
|
return pgtype.Text{String: s, Valid: true}
|
|
default:
|
|
return pgtype.Text{Valid: false}
|
|
}
|
|
}
|
|
|
|
func optionalInt4Update(v *int) pgtype.Int4 {
|
|
if v == nil {
|
|
return pgtype.Int4{Valid: false}
|
|
}
|
|
return pgtype.Int4{Int32: int32(*v), Valid: true}
|
|
}
|
|
|
|
func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, error) {
|
|
sortParam := optionalInt4Update(input.SortOrder)
|
|
if input.SortOrder != nil {
|
|
cur, err := s.GetProgramByID(ctx, id)
|
|
if err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
oldPos := int32(cur.SortOrder)
|
|
newPos := int32(*input.SortOrder)
|
|
if oldPos != newPos {
|
|
q, tx, err := s.BeginTx(ctx)
|
|
if err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
if err := repositionProgramSortOrder(ctx, tx, id, oldPos, newPos); err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
var nameText pgtype.Text
|
|
if input.Name != nil {
|
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
|
} else {
|
|
nameText = pgtype.Text{Valid: false}
|
|
}
|
|
p, err := q.UpdateProgram(ctx, dbgen.UpdateProgramParams{
|
|
ID: id,
|
|
Name: nameText,
|
|
Description: optionalTextUpdate(input.Description),
|
|
Category: optionalTextUpdate(input.Category),
|
|
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
|
SortOrder: pgtype.Int4{Valid: false},
|
|
})
|
|
if err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return domain.Program{}, err
|
|
}
|
|
return programToDomain(p), nil
|
|
}
|
|
sortParam = pgtype.Int4{Valid: false}
|
|
}
|
|
|
|
var nameText pgtype.Text
|
|
if input.Name != nil {
|
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
|
} else {
|
|
nameText = pgtype.Text{Valid: false}
|
|
}
|
|
p, err := s.queries.UpdateProgram(ctx, dbgen.UpdateProgramParams{
|
|
ID: id,
|
|
Name: nameText,
|
|
Description: optionalTextUpdate(input.Description),
|
|
Category: optionalTextUpdate(input.Category),
|
|
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
|
SortOrder: sortParam,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.Program{}, pgx.ErrNoRows
|
|
}
|
|
return domain.Program{}, err
|
|
}
|
|
return programToDomain(p), nil
|
|
}
|
|
|
|
func (s *Store) DeleteProgram(ctx context.Context, id int64) error {
|
|
return s.queries.DeleteProgram(ctx, id)
|
|
}
|