diff --git a/db/query/lms_courses.sql b/db/query/lms_courses.sql index 7f8d612..ed86aeb 100644 --- a/db/query/lms_courses.sql +++ b/db/query/lms_courses.sql @@ -1,16 +1,17 @@ -- name: CreateCourse :one INSERT INTO courses (program_id, name, description, thumbnail, sort_order) SELECT - $1, - $2, - $3, - $4, - coalesce(( - SELECT - max(c.sort_order) - FROM courses c - WHERE - c.program_id = $1), 0) + 1 + sqlc.arg('program_id'), + sqlc.arg('name'), + sqlc.arg('description'), + sqlc.arg('thumbnail'), + COALESCE(sqlc.narg('sort_order')::int, + COALESCE(( + SELECT + max(c.sort_order) + FROM courses c + WHERE + c.program_id = sqlc.arg('program_id')), 0) + 1) RETURNING *; diff --git a/docs/docs.go b/docs/docs.go index b7c799d..321c4f4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4395,7 +4395,7 @@ const docTemplate = `{ } }, "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": [ "application/json" ], @@ -10364,6 +10364,11 @@ const docTemplate = `{ "name": { "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": { "type": "string" } diff --git a/docs/swagger.json b/docs/swagger.json index 1fce5e9..73c0fa0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4387,7 +4387,7 @@ } }, "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": [ "application/json" ], @@ -10356,6 +10356,11 @@ "name": { "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": { "type": "string" } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7e3bf66..dcd9188 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -338,6 +338,11 @@ definitions: type: string name: 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: type: string required: @@ -5372,7 +5377,9 @@ paths: post: consumes: - 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: - description: Program ID in: path diff --git a/gen/db/lms_courses.sql.go b/gen/db/lms_courses.sql.go index 3d96a13..e812f85 100644 --- a/gen/db/lms_courses.sql.go +++ b/gen/db/lms_courses.sql.go @@ -18,12 +18,13 @@ SELECT $2, $3, $4, - coalesce(( - SELECT - max(c.sort_order) - FROM courses c - WHERE - c.program_id = $1), 0) + 1 + COALESCE($5::int, + COALESCE(( + SELECT + max(c.sort_order) + FROM courses c + WHERE + c.program_id = $1), 0) + 1) RETURNING id, program_id, name, description, thumbnail, created_at, updated_at, sort_order ` @@ -33,6 +34,7 @@ type CreateCourseParams struct { Name string `json:"name"` Description pgtype.Text `json:"description"` Thumbnail pgtype.Text `json:"thumbnail"` + SortOrder pgtype.Int4 `json:"sort_order"` } 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.Description, arg.Thumbnail, + arg.SortOrder, ) var i Course err := row.Scan( diff --git a/internal/domain/course.go b/internal/domain/course.go index 541f91f..768bed6 100644 --- a/internal/domain/course.go +++ b/internal/domain/course.go @@ -25,10 +25,10 @@ type Course struct { UpdatedAt *time.Time `json:"updated_at,omitempty"` // 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). - ModuleCount int `json:"module_count"` - LessonCount int `json:"lesson_count"` - PracticeCount int `json:"practice_count"` - HasPractice bool `json:"has_practice"` + ModuleCount int `json:"module_count"` + LessonCount int `json:"lesson_count"` + PracticeCount int `json:"practice_count"` + HasPractice bool `json:"has_practice"` Access *LMSEntityAccess `json:"access,omitempty"` } @@ -36,6 +36,8 @@ type CreateCourseInput struct { Name string `json:"name" validate:"required"` Description *string `json:"description,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 { diff --git a/internal/repository/lms_courses.go b/internal/repository/lms_courses.go index 55f7c26..73ef759 100644 --- a/internal/repository/lms_courses.go +++ b/internal/repository/lms_courses.go @@ -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) { + 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{ ProgramID: programID, Name: input.Name, Description: toPgText(input.Description), Thumbnail: toPgText(input.Thumbnail), + SortOrder: pgtype.Int4{Valid: false}, }) if err != nil { return domain.Course{}, err diff --git a/internal/web_server/handlers/course_handler.go b/internal/web_server/handlers/course_handler.go index 80d4fec..1d9f8c9 100644 --- a/internal/web_server/handlers/course_handler.go +++ b/internal/web_server/handlers/course_handler.go @@ -13,7 +13,7 @@ import ( // CreateCourse godoc // @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 // @Accept json // @Produce json