From d28bddace16df292d1abc192c34a12692ef85ca2 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 19 May 2026 02:10:49 -0700 Subject: [PATCH] Accept optional sort_order when creating LMS programs. Preserve append-after-max ordering when omitting sort_order and keep global uniqueness enforced by DB. Co-authored-by: Cursor --- db/query/programs.sql | 10 +++++----- docs/docs.go | 7 ++++++- docs/swagger.json | 7 ++++++- docs/swagger.yaml | 9 ++++++++- gen/db/programs.sql.go | 12 +++++++++--- internal/domain/program.go | 16 +++++++++------- internal/repository/programs.go | 1 + internal/web_server/handlers/program_handler.go | 2 +- 8 files changed, 45 insertions(+), 19 deletions(-) diff --git a/db/query/programs.sql b/db/query/programs.sql index f2b79bf..dd3ca74 100644 --- a/db/query/programs.sql +++ b/db/query/programs.sql @@ -1,13 +1,13 @@ -- name: CreateProgram :one INSERT INTO programs (name, description, thumbnail, sort_order) SELECT - $1, - $2, - $3, - coalesce(( + sqlc.arg('name'), + sqlc.arg('description'), + sqlc.arg('thumbnail'), + COALESCE(sqlc.narg('sort_order')::int, COALESCE(( SELECT max(p.sort_order) - FROM programs AS p), 0) + 1 + FROM programs AS p), 0) + 1) RETURNING *; diff --git a/docs/docs.go b/docs/docs.go index 639a6cc..b7c799d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4150,7 +4150,7 @@ const docTemplate = `{ } }, "post": { - "description": "Create a top-level LMS program", + "description": "Create a top-level LMS program. Optional sort_order inserts at that global ordering; omit it to append after the current highest sort_order. Unique constraint applies to sort_order.", "consumes": [ "application/json" ], @@ -10563,6 +10563,11 @@ const docTemplate = `{ "name": { "type": "string" }, + "sort_order": { + "description": "SortOrder inserts at this global program order when set; omit to append after current max (sort_order uniqueness is enforced).", + "type": "integer", + "minimum": 0 + }, "thumbnail": { "type": "string" } diff --git a/docs/swagger.json b/docs/swagger.json index fb4f445..1fce5e9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4142,7 +4142,7 @@ } }, "post": { - "description": "Create a top-level LMS program", + "description": "Create a top-level LMS program. Optional sort_order inserts at that global ordering; omit it to append after the current highest sort_order. Unique constraint applies to sort_order.", "consumes": [ "application/json" ], @@ -10555,6 +10555,11 @@ "name": { "type": "string" }, + "sort_order": { + "description": "SortOrder inserts at this global program order when set; omit to append after current max (sort_order uniqueness is enforced).", + "type": "integer", + "minimum": 0 + }, "thumbnail": { "type": "string" } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3f08e03..7e3bf66 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -468,6 +468,11 @@ definitions: type: string name: type: string + sort_order: + description: SortOrder inserts at this global program order when set; omit + to append after current max (sort_order uniqueness is enforced). + minimum: 0 + type: integer thumbnail: type: string required: @@ -5230,7 +5235,9 @@ paths: post: consumes: - application/json - description: Create a top-level LMS program + description: Create a top-level LMS program. Optional sort_order inserts at + that global ordering; omit it to append after the current highest sort_order. + Unique constraint applies to sort_order. parameters: - description: Program in: body diff --git a/gen/db/programs.sql.go b/gen/db/programs.sql.go index 96c8ebf..907e4e6 100644 --- a/gen/db/programs.sql.go +++ b/gen/db/programs.sql.go @@ -17,10 +17,10 @@ SELECT $1, $2, $3, - coalesce(( + COALESCE($4::int, COALESCE(( SELECT max(p.sort_order) - FROM programs AS p), 0) + 1 + FROM programs AS p), 0) + 1) RETURNING id, name, description, thumbnail, created_at, updated_at, sort_order ` @@ -29,10 +29,16 @@ type CreateProgramParams struct { Name string `json:"name"` Description pgtype.Text `json:"description"` Thumbnail pgtype.Text `json:"thumbnail"` + SortOrder pgtype.Int4 `json:"sort_order"` } func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) { - row := q.db.QueryRow(ctx, CreateProgram, arg.Name, arg.Description, arg.Thumbnail) + row := q.db.QueryRow(ctx, CreateProgram, + arg.Name, + arg.Description, + arg.Thumbnail, + arg.SortOrder, + ) var i Program err := row.Scan( &i.ID, diff --git a/internal/domain/program.go b/internal/domain/program.go index da79e49..4bddd45 100644 --- a/internal/domain/program.go +++ b/internal/domain/program.go @@ -4,13 +4,13 @@ import "time" // Program is the top-level container in the LMS hierarchy (e.g. tracks like Beginner / Intermediate / Advanced). type Program struct { - ID int64 `json:"id"` - Name string `json:"name"` - Description *string `json:"description,omitempty"` - Thumbnail *string `json:"thumbnail,omitempty"` - SortOrder int `json:"sort_order"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` + ID int64 `json:"id"` + Name string `json:"name"` + Description *string `json:"description,omitempty"` + Thumbnail *string `json:"thumbnail,omitempty"` + SortOrder int `json:"sort_order"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` Access *LMSEntityAccess `json:"access,omitempty"` } @@ -18,6 +18,8 @@ type CreateProgramInput struct { Name string `json:"name" validate:"required"` Description *string `json:"description,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"` + // SortOrder inserts at this global program order when set; omit to append after current max (sort_order uniqueness is enforced). + SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"` } type UpdateProgramInput struct { diff --git a/internal/repository/programs.go b/internal/repository/programs.go index cfa4191..5bfafc7 100644 --- a/internal/repository/programs.go +++ b/internal/repository/programs.go @@ -32,6 +32,7 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp Name: input.Name, Description: toPgText(input.Description), Thumbnail: toPgText(input.Thumbnail), + SortOrder: optionalInt4Update(input.SortOrder), }) if err != nil { return domain.Program{}, err diff --git a/internal/web_server/handlers/program_handler.go b/internal/web_server/handlers/program_handler.go index 362e028..1296ae1 100644 --- a/internal/web_server/handlers/program_handler.go +++ b/internal/web_server/handlers/program_handler.go @@ -12,7 +12,7 @@ import ( // CreateProgram godoc // @Summary Create program -// @Description Create a top-level LMS program +// @Description Create a top-level LMS program. Optional sort_order inserts at that global ordering; omit it to append after the current highest sort_order. Unique constraint applies to sort_order. // @Tags programs // @Accept json // @Produce json