Honor optional sort_order on module create under a course.

Accept sort_order in CreateModuleInput, shift siblings when set, and default to max+1 when omitted.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-19 04:15:18 -07:00
parent 12ad59c409
commit 83db13bed0
4 changed files with 54 additions and 17 deletions

View File

@ -1,17 +1,18 @@
-- name: CreateModule :one -- name: CreateModule :one
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order) INSERT INTO modules (program_id, course_id, name, description, icon, sort_order)
SELECT SELECT
$1, sqlc.arg('program_id'),
$2, sqlc.arg('course_id'),
$3, sqlc.arg('name'),
$4, sqlc.arg('description'),
$5, sqlc.arg('icon'),
coalesce(( COALESCE(sqlc.narg('sort_order')::int,
SELECT COALESCE((
max(m.sort_order) SELECT
FROM modules m max(m.sort_order)
WHERE FROM modules m
m.course_id = $2), 0) + 1 WHERE
m.course_id = sqlc.arg('course_id')), 0) + 1)
RETURNING RETURNING
*; *;

View File

@ -19,12 +19,13 @@ SELECT
$3, $3,
$4, $4,
$5, $5,
coalesce(( COALESCE($6::int,
SELECT COALESCE((
max(m.sort_order) SELECT
FROM modules m max(m.sort_order)
WHERE FROM modules m
m.course_id = $2), 0) + 1 WHERE
m.course_id = $2), 0) + 1)
RETURNING RETURNING
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
` `
@ -35,6 +36,7 @@ type CreateModuleParams struct {
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
} }
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) { func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
@ -44,6 +46,7 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod
arg.Name, arg.Name,
arg.Description, arg.Description,
arg.Icon, arg.Icon,
arg.SortOrder,
) )
var i Module var i Module
err := row.Scan( err := row.Scan(

View File

@ -21,6 +21,8 @@ type CreateModuleInput struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
// SortOrder within the course when set; omit to append after current max within course_id (uniqueness is per-course).
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
} }
type UpdateModuleInput struct { type UpdateModuleInput struct {

View File

@ -30,12 +30,43 @@ func moduleToDomain(m dbgen.Module) domain.Module {
} }
func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) { func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx)
if err != nil {
return domain.Module{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx,
`UPDATE modules SET sort_order = sort_order + 1 WHERE course_id = $1 AND sort_order >= $2`,
courseID, target,
); err != nil {
return domain.Module{}, err
}
m, err := q.CreateModule(ctx, dbgen.CreateModuleParams{
ProgramID: programID,
CourseID: courseID,
Name: input.Name,
Description: toPgText(input.Description),
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
})
if err != nil {
return domain.Module{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.Module{}, err
}
return moduleToDomain(m), nil
}
m, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{ m, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{
ProgramID: programID, ProgramID: programID,
CourseID: courseID, CourseID: courseID,
Name: input.Name, Name: input.Name,
Description: toPgText(input.Description), Description: toPgText(input.Description),
Icon: toPgText(input.Icon), Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Valid: false},
}) })
if err != nil { if err != nil {
return domain.Module{}, err return domain.Module{}, err