Honor optional sort_order on course create under a program.

Parses body sort_order, shifts sibling courses in-program, and inserts at the requested slot; omitting it keeps append-after-max behavior. Swagger/sqlc regenerated.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-19 02:54:17 -07:00
parent 1136a166f5
commit 37aef49e28
8 changed files with 77 additions and 24 deletions

View File

@ -1,16 +1,17 @@
-- name: CreateCourse :one -- name: CreateCourse :one
INSERT INTO courses (program_id, name, description, thumbnail, sort_order) INSERT INTO courses (program_id, name, description, thumbnail, sort_order)
SELECT SELECT
$1, sqlc.arg('program_id'),
$2, sqlc.arg('name'),
$3, sqlc.arg('description'),
$4, sqlc.arg('thumbnail'),
coalesce(( COALESCE(sqlc.narg('sort_order')::int,
SELECT COALESCE((
max(c.sort_order) SELECT
FROM courses c max(c.sort_order)
WHERE FROM courses c
c.program_id = $1), 0) + 1 WHERE
c.program_id = sqlc.arg('program_id')), 0) + 1)
RETURNING RETURNING
*; *;

View File

@ -4395,7 +4395,7 @@ const docTemplate = `{
} }
}, },
"post": { "post": {
"description": "Create a course under a program", "description": "Create a course under a program. Optional sort_order assigns position within that program (siblings shifted); omit to append after the current highest sort_order in the program.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -10364,6 +10364,11 @@ const docTemplate = `{
"name": { "name": {
"type": "string" "type": "string"
}, },
"sort_order": {
"description": "SortOrder within the program when set; omit to append after current max within program_id (uniqueness is per-program).",
"type": "integer",
"minimum": 0
},
"thumbnail": { "thumbnail": {
"type": "string" "type": "string"
} }

View File

@ -4387,7 +4387,7 @@
} }
}, },
"post": { "post": {
"description": "Create a course under a program", "description": "Create a course under a program. Optional sort_order assigns position within that program (siblings shifted); omit to append after the current highest sort_order in the program.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -10356,6 +10356,11 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"sort_order": {
"description": "SortOrder within the program when set; omit to append after current max within program_id (uniqueness is per-program).",
"type": "integer",
"minimum": 0
},
"thumbnail": { "thumbnail": {
"type": "string" "type": "string"
} }

View File

@ -338,6 +338,11 @@ definitions:
type: string type: string
name: name:
type: string type: string
sort_order:
description: SortOrder within the program when set; omit to append after current
max within program_id (uniqueness is per-program).
minimum: 0
type: integer
thumbnail: thumbnail:
type: string type: string
required: required:
@ -5372,7 +5377,9 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: Create a course under a program description: Create a course under a program. Optional sort_order assigns position
within that program (siblings shifted); omit to append after the current highest
sort_order in the program.
parameters: parameters:
- description: Program ID - description: Program ID
in: path in: path

View File

@ -18,12 +18,13 @@ SELECT
$2, $2,
$3, $3,
$4, $4,
coalesce(( COALESCE($5::int,
SELECT COALESCE((
max(c.sort_order) SELECT
FROM courses c max(c.sort_order)
WHERE FROM courses c
c.program_id = $1), 0) + 1 WHERE
c.program_id = $1), 0) + 1)
RETURNING RETURNING
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
` `
@ -33,6 +34,7 @@ type CreateCourseParams struct {
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
} }
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) { func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
@ -41,6 +43,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
arg.Name, arg.Name,
arg.Description, arg.Description,
arg.Thumbnail, arg.Thumbnail,
arg.SortOrder,
) )
var i Course var i Course
err := row.Scan( err := row.Scan(

View File

@ -25,10 +25,10 @@ type Course struct {
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
// Populated on list-by-program. Practice count: lms_practices rows with course_id = course only // Populated on list-by-program. Practice count: lms_practices rows with course_id = course only
// (not practices attached to a module or lesson under this course). // (not practices attached to a module or lesson under this course).
ModuleCount int `json:"module_count"` ModuleCount int `json:"module_count"`
LessonCount int `json:"lesson_count"` LessonCount int `json:"lesson_count"`
PracticeCount int `json:"practice_count"` PracticeCount int `json:"practice_count"`
HasPractice bool `json:"has_practice"` HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"` Access *LMSEntityAccess `json:"access,omitempty"`
} }
@ -36,6 +36,8 @@ type CreateCourseInput struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
// SortOrder within the program when set; omit to append after current max within program_id (uniqueness is per-program).
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
} }
type UpdateCourseInput struct { type UpdateCourseInput struct {

View File

@ -29,11 +29,41 @@ func courseToDomain(c dbgen.Course) domain.Course {
} }
func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) { func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) {
if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx)
if err != nil {
return domain.Course{}, err
}
defer func() { _ = tx.Rollback(ctx) }()
target := int32(*input.SortOrder)
if _, err := tx.Exec(ctx,
`UPDATE courses SET sort_order = sort_order + 1 WHERE program_id = $1 AND sort_order >= $2`,
programID, target,
); err != nil {
return domain.Course{}, err
}
c, err := q.CreateCourse(ctx, dbgen.CreateCourseParams{
ProgramID: programID,
Name: input.Name,
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
})
if err != nil {
return domain.Course{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.Course{}, err
}
return courseToDomain(c), nil
}
c, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{ c, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{
ProgramID: programID, ProgramID: programID,
Name: input.Name, Name: input.Name,
Description: toPgText(input.Description), Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail), Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
}) })
if err != nil { if err != nil {
return domain.Course{}, err return domain.Course{}, err

View File

@ -13,7 +13,7 @@ import (
// CreateCourse godoc // CreateCourse godoc
// @Summary Create course // @Summary Create course
// @Description Create a course under a program // @Description Create a course under a program. Optional sort_order assigns position within that program (siblings shifted); omit to append after the current highest sort_order in the program.
// @Tags courses // @Tags courses
// @Accept json // @Accept json
// @Produce json // @Produce json