REFACTOR: Optimized Catagory IDs
Build, Push and Deploy CMS / build-push-deploy (push) Successful in 2m19s

This commit is contained in:
FahdShalhoub
2026-08-17 00:51:11 +03:00
parent aa32bb6ed8
commit 56a2992206
9 changed files with 373 additions and 264 deletions
+77 -15
View File
@@ -1,6 +1,7 @@
package handlers
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
@@ -8,6 +9,7 @@ import (
"log"
"net/http"
"path/filepath"
"strconv"
"strings"
"thamanyah/cms/v2/internal/api"
"thamanyah/cms/v2/internal/services"
@@ -23,7 +25,7 @@ const (
// ListCategories returns the categories a video may be assigned to.
//
// @Summary List video categories
// @Description Returns the fixed lookup set of categories a video can belong to. Values from this list are the only ones accepted in the `categories` field of POST /api/videos.
// @Description Returns the fixed lookup set of categories a video can belong to, each with the id used to reference it. The `id` values from this list are the only ones accepted in the `categoryIds` field of POST /api/videos.
// @Tags categories
// @Produce json
// @Success 200 {object} api.CategoriesResponse
@@ -38,10 +40,11 @@ func ListCategories(w http.ResponseWriter, r *http.Request) {
return
}
if categories == nil {
categories = []string{}
body := make([]api.Category, 0, len(categories))
for _, category := range categories {
body = append(body, api.Category{ID: category.ID, Name: category.Name})
}
writeJSON(w, http.StatusOK, api.CategoriesResponse{Categories: categories})
writeJSON(w, http.StatusOK, api.CategoriesResponse{Categories: body})
}
// allowedUploadContentTypes is the set of media types accepted for upload,
@@ -108,11 +111,11 @@ func PresignVideoUpload(w http.ResponseWriter, r *http.Request) {
// @Tags videos
// @Accept json
// @Produce json
// @Param request body api.CompleteRequest true "Video metadata plus the storage key from the presign step. title and key are required; categories must be values from GET /api/categories."
// @Success 201 {object} api.VideoResponse
// @Param request body api.CompleteRequest true "Video metadata plus the storage key from the presign step. title, key and categoryIds are required; categoryIds must hold at least one id from GET /api/categories."
// @Success 201 {object} api.CompleteResponse
// @Failure 400 {object} api.ProblemDetails "Request body was not valid JSON"
// @Failure 422 {object} api.ProblemDetails "title was empty, or key was missing or not a key issued by this API"
// @Failure 500 {object} api.ProblemDetails "Transcoding job could not be queued, or the video record could not be saved"
// @Failure 422 {object} api.ProblemDetails "title was empty, key was missing or not a key issued by this API, or categoryIds was empty or contained an id that does not exist"
// @Failure 500 {object} api.ProblemDetails "Categories could not be read, the transcoding job could not be queued, or the video record could not be saved"
// @Router /api/videos [post]
func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBodySize)
@@ -138,11 +141,23 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
return
}
categories := make([]string, 0, len(req.Categories))
for _, category := range req.Categories {
if category = strings.TrimSpace(category); category != "" {
categories = append(categories, category)
}
if len(req.CategoryIDs) == 0 {
writeProblem(w, http.StatusUnprocessableEntity, "At least one category is required.",
"The 'categoryIds' field was absent or empty. Every video belongs to at least one category; send ids taken from the 'id' field of GET /api/categories.")
return
}
categoryIDs, unknown, err := resolveCategoryIDs(r.Context(), req.CategoryIDs)
if err != nil {
log.Printf("Something Went Wrong Loading Categories: %s", err)
writeProblem(w, http.StatusInternalServerError, "Something Went Wrong Loading Categories",
"The submitted category ids could not be checked against the database, so no video record was created. This is a server-side fault; the uploaded file is still in storage, so retry this request with the same 'key'.")
return
}
if len(unknown) > 0 {
writeProblem(w, http.StatusUnprocessableEntity, "Unknown category.",
fmt.Sprintf("The 'categoryIds' field referenced %s, which do not exist. Send ids taken from the 'id' field of GET /api/categories.", formatCategoryIDs(unknown)))
return
}
jobID, err := services.MediaConvertClient.QueueEncodingJob(r.Context(), key)
@@ -156,7 +171,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
video, err := services.DB.CreateVideo(r.Context(), services.Video{
Title: title,
Description: strings.TrimSpace(req.Description),
Categories: categories,
CategoryIDs: categoryIDs,
Tags: strings.TrimSpace(req.Tags),
FileName: strings.TrimSpace(req.FileName),
StorageKey: key,
@@ -175,7 +190,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
ID: video.ID,
Title: video.Title,
Description: video.Description,
Categories: video.Categories,
CategoryIDs: video.CategoryIDs,
Tags: video.Tags,
FileName: video.FileName,
StorageKey: video.StorageKey,
@@ -187,6 +202,53 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) {
})
}
// resolveCategoryIDs deduplicates the submitted category ids, preserving the
// order they arrived in, and splits them into ids that exist and ids that do
// not. Checking them here rather than leaning on the video_categories foreign
// key keeps an unknown id from surfacing only after a transcoding job has
// already been queued — and the duplicate pass keeps a repeated id from
// tripping the join table's composite primary key. Callers reject an empty
// list before calling, so a non-empty submitted list always yields either at
// least one known id or at least one unknown one.
func resolveCategoryIDs(ctx context.Context, submitted []int16) (known, unknown []int16, err error) {
known = make([]int16, 0, len(submitted))
categories, err := services.DB.ListCategories(ctx)
if err != nil {
return nil, nil, err
}
exists := make(map[int16]struct{}, len(categories))
for _, category := range categories {
exists[category.ID] = struct{}{}
}
seen := make(map[int16]struct{}, len(submitted))
for _, id := range submitted {
if _, duplicate := seen[id]; duplicate {
continue
}
seen[id] = struct{}{}
if _, ok := exists[id]; ok {
known = append(known, id)
} else {
unknown = append(unknown, id)
}
}
return known, unknown, nil
}
func formatCategoryIDs(ids []int16) string {
formatted := make([]string, 0, len(ids))
for _, id := range ids {
formatted = append(formatted, strconv.Itoa(int(id)))
}
if len(formatted) == 1 {
return "id " + formatted[0]
}
return "ids " + strings.Join(formatted, ", ")
}
func randomFilename(original string) (string, error) {
ext := filepath.Ext(filepath.Base(original))
buf := make([]byte, 16)