From 4132c6071ec1552b357886e76f713dc66a934f08 Mon Sep 17 00:00:00 2001 From: FahdShalhoub Date: Wed, 26 Aug 2026 08:51:40 +0300 Subject: [PATCH] REFACTOR: Seperated Catagories And Video DB Repositories --- cms/internal/db/client.go | 30 ++++ cms/internal/db/repositories/catagories.go | 54 +++++++ cms/internal/db/repositories/videos.go | 52 +++++++ cms/internal/handlers/videos.go | 14 +- cms/internal/models/catagory.go | 6 + .../{services/status.go => models/video.go} | 19 ++- cms/internal/services/db.go | 141 ------------------ cms/main.go | 21 ++- 8 files changed, 182 insertions(+), 155 deletions(-) create mode 100644 cms/internal/db/client.go create mode 100644 cms/internal/db/repositories/catagories.go create mode 100644 cms/internal/db/repositories/videos.go create mode 100644 cms/internal/models/catagory.go rename cms/internal/{services/status.go => models/video.go} (71%) delete mode 100644 cms/internal/services/db.go diff --git a/cms/internal/db/client.go b/cms/internal/db/client.go new file mode 100644 index 0000000..d99244f --- /dev/null +++ b/cms/internal/db/client.go @@ -0,0 +1,30 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "log" +) + +func CreateDBConnection(connectionString string) *sql.DB { + sqlDB, err := sql.Open("postgres", connectionString) + if err != nil { + panic(fmt.Errorf("db: cannot open connection: %w", err)) + } + + return sqlDB +} + +func CloseConnection(SQLDB *sql.DB) { + err := SQLDB.Close() + if err != nil { + log.Fatalf("Error Closing DB Connection: %s", err) + } +} + +func AssertSuccessfulConnection(ctx context.Context, SQLDB *sql.DB) { + if err := SQLDB.PingContext(ctx); err != nil { + panic(fmt.Errorf("db: cannot connect: %w", err)) + } +} diff --git a/cms/internal/db/repositories/catagories.go b/cms/internal/db/repositories/catagories.go new file mode 100644 index 0000000..46e8313 --- /dev/null +++ b/cms/internal/db/repositories/catagories.go @@ -0,0 +1,54 @@ +package repositories + +import ( + "context" + "database/sql" + "thamanyah/cms/v2/internal/models" +) + +type CatagoriesRepository interface { + ListCategories(ctx context.Context) ([]models.Category, error) + ListCategoriesIDs(ctx context.Context) ([]int16, error) +} + +var CatagoriesRepo CatagoriesRepository + +type ConcreteCatagoriesRepository struct { + SQLDB *sql.DB +} + +func (svc ConcreteCatagoriesRepository) ListCategories(ctx context.Context) ([]models.Category, error) { + rows, err := svc.SQLDB.QueryContext(ctx, `SELECT id, name FROM categories ORDER BY name`) + if err != nil { + return nil, err + } + defer rows.Close() + + var categories []models.Category + for rows.Next() { + var category models.Category + if err := rows.Scan(&category.ID, &category.Name); err != nil { + return nil, err + } + categories = append(categories, category) + } + return categories, rows.Err() +} + +func (svc ConcreteCatagoriesRepository) ListCategoriesIDs(ctx context.Context) ([]int16, error) { + rows, err := svc.SQLDB.QueryContext(ctx, `SELECT id FROM categories`) + if err != nil { + return nil, err + } + defer rows.Close() + + var categories []int16 + for rows.Next() { + var category int16 + if err := rows.Scan(&category); err != nil { + return nil, err + } + categories = append(categories, category) + } + return categories, rows.Err() +} diff --git a/cms/internal/db/repositories/videos.go b/cms/internal/db/repositories/videos.go new file mode 100644 index 0000000..ca553e1 --- /dev/null +++ b/cms/internal/db/repositories/videos.go @@ -0,0 +1,52 @@ +package repositories + +import ( + "context" + "database/sql" + "fmt" + "strings" + "thamanyah/cms/v2/internal/models" +) + +type VideoRepository interface { + CreateVideo(ctx context.Context, v models.Video) (models.Video, error) +} + +var VideoRepo VideoRepository + +type ConcreteVideoRepository struct { + SQLDB *sql.DB +} + +func (svc ConcreteVideoRepository) CreateVideo(ctx context.Context, v models.Video) (models.Video, error) { + tx, err := svc.SQLDB.BeginTx(ctx, nil) + if err != nil { + return models.Video{}, err + } + defer tx.Rollback() + + row := tx.QueryRowContext(ctx, ` + INSERT INTO videos (title, description, tags, file_name, storage_key, mediaconvert_job_id, status, size_bytes) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id, created_at, updated_at + `, v.Title, v.Description, v.Tags, v.FileName, v.StorageKey, v.MediaConvertJobID, string(v.Status), v.SizeBytes) + + if err := row.Scan(&v.ID, &v.CreatedAt, &v.UpdatedAt); err != nil { + return models.Video{}, err + } + + rows := make([]string, 0, len(v.CategoryIDs)) + for _, categoryID := range v.CategoryIDs { + rows = append(rows, fmt.Sprintf("($1, %d)", categoryID)) + } + + if _, err := tx.ExecContext(ctx, `INSERT INTO video_categories (video_id, category_id) VALUES `+strings.Join(rows, ", "), v.ID); err != nil { + return models.Video{}, fmt.Errorf("linking categories %v: %w", v.CategoryIDs, err) + } + + if err := tx.Commit(); err != nil { + return models.Video{}, err + } + + return v, nil +} diff --git a/cms/internal/handlers/videos.go b/cms/internal/handlers/videos.go index 65ecf74..c2d295f 100644 --- a/cms/internal/handlers/videos.go +++ b/cms/internal/handlers/videos.go @@ -13,6 +13,8 @@ import ( "strconv" "strings" "thamanyah/cms/v2/internal/api" + "thamanyah/cms/v2/internal/db/repositories" + "thamanyah/cms/v2/internal/models" "thamanyah/cms/v2/internal/services" "time" ) @@ -33,7 +35,7 @@ const ( // @Failure 500 {object} api.ProblemDetails "Categories could not be read from the database" // @Router /api/categories [get] func ListCategories(w http.ResponseWriter, r *http.Request) { - categories, err := services.DB.ListCategories(r.Context()) + categories, err := repositories.CatagoriesRepo.ListCategories(r.Context()) if err != nil { log.Printf("Something Went Wrong Loading Categories: %s", err) writeProblem(w, http.StatusInternalServerError, "Something Went Wrong Loading Categories", @@ -143,7 +145,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) { return } - err := resolveCategoryIDs(r.Context(), req.CategoryIDs) + err := validateCatagoryIDS(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", @@ -159,7 +161,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) { return } - video, err := services.DB.CreateVideo(r.Context(), services.Video{ + video, err := repositories.VideoRepo.CreateVideo(r.Context(), models.Video{ Title: title, Description: strings.TrimSpace(req.Description), CategoryIDs: req.CategoryIDs, @@ -167,7 +169,7 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) { FileName: strings.TrimSpace(req.FileName), StorageKey: key, MediaConvertJobID: jobID, - Status: services.VideoStatusProcessing, + Status: models.VideoStatusProcessing, SizeBytes: 60, }) if err != nil { @@ -193,8 +195,8 @@ func CompleteVideoUpload(w http.ResponseWriter, r *http.Request) { }) } -func resolveCategoryIDs(ctx context.Context, submitted []int16) (err error) { - categoryIDS, err := services.DB.ListCategoriesIDs(ctx) +func validateCatagoryIDS(ctx context.Context, submitted []int16) (err error) { + categoryIDS, err := repositories.CatagoriesRepo.ListCategoriesIDs(ctx) if err != nil { return err } diff --git a/cms/internal/models/catagory.go b/cms/internal/models/catagory.go new file mode 100644 index 0000000..7cff802 --- /dev/null +++ b/cms/internal/models/catagory.go @@ -0,0 +1,6 @@ +package models + +type Category struct { + ID int16 + Name string +} diff --git a/cms/internal/services/status.go b/cms/internal/models/video.go similarity index 71% rename from cms/internal/services/status.go rename to cms/internal/models/video.go index b2cc7aa..128753e 100644 --- a/cms/internal/services/status.go +++ b/cms/internal/models/video.go @@ -1,4 +1,21 @@ -package services +package models + +import "time" + +type Video struct { + ID string + Title string + Description string + CategoryIDs []int16 + Tags string + FileName string + StorageKey string + MediaConvertJobID string + Status VideoStatus + SizeBytes int64 + CreatedAt time.Time + UpdatedAt time.Time +} // VideoStatus is the transcoding lifecycle state of a video, stored verbatim // in videos.status. The set is closed on both sides: the database rejects diff --git a/cms/internal/services/db.go b/cms/internal/services/db.go deleted file mode 100644 index fac6cdb..0000000 --- a/cms/internal/services/db.go +++ /dev/null @@ -1,141 +0,0 @@ -package services - -import ( - "context" - "database/sql" - "fmt" - "log" - "strings" - "thamanyah/cms/v2/internal/db" - "time" -) - -var DB DBClient - -type Category struct { - ID int16 - Name string -} - -type Video struct { - ID string - Title string - Description string - CategoryIDs []int16 - Tags string - FileName string - StorageKey string - MediaConvertJobID string - Status VideoStatus - SizeBytes int64 - CreatedAt time.Time - UpdatedAt time.Time -} - -type DBClient interface { - CreateVideo(ctx context.Context, v Video) (Video, error) - ListCategories(ctx context.Context) ([]Category, error) - ListCategoriesIDs(ctx context.Context) ([]int16, error) -} - -type DBConcrete struct { - SQLDB *sql.DB -} - -func CreateConcreteDbClient(connectionString string) DBConcrete { - sqlDB, err := sql.Open("postgres", connectionString) - if err != nil { - panic(fmt.Errorf("db: cannot open connection: %w", err)) - } - return DBConcrete{ - SQLDB: sqlDB, - } -} - -func (svc DBConcrete) CreateVideo(ctx context.Context, v Video) (Video, error) { - tx, err := svc.SQLDB.BeginTx(ctx, nil) - if err != nil { - return Video{}, err - } - defer tx.Rollback() - - row := tx.QueryRowContext(ctx, ` - INSERT INTO videos (title, description, tags, file_name, storage_key, mediaconvert_job_id, status, size_bytes) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING id, created_at, updated_at - `, v.Title, v.Description, v.Tags, v.FileName, v.StorageKey, v.MediaConvertJobID, string(v.Status), v.SizeBytes) - - if err := row.Scan(&v.ID, &v.CreatedAt, &v.UpdatedAt); err != nil { - return Video{}, err - } - - rows := make([]string, 0, len(v.CategoryIDs)) - for _, categoryID := range v.CategoryIDs { - rows = append(rows, fmt.Sprintf("($1, %d)", categoryID)) - } - - if _, err := tx.ExecContext(ctx, `INSERT INTO video_categories (video_id, category_id) VALUES `+strings.Join(rows, ", "), v.ID); err != nil { - return Video{}, fmt.Errorf("linking categories %v: %w", v.CategoryIDs, err) - } - - if err := tx.Commit(); err != nil { - return Video{}, err - } - - return v, nil -} - -func (svc DBConcrete) ListCategories(ctx context.Context) ([]Category, error) { - rows, err := svc.SQLDB.QueryContext(ctx, `SELECT id, name FROM categories ORDER BY name`) - if err != nil { - return nil, err - } - defer rows.Close() - - var categories []Category - for rows.Next() { - var category Category - if err := rows.Scan(&category.ID, &category.Name); err != nil { - return nil, err - } - categories = append(categories, category) - } - return categories, rows.Err() -} - -func (svc DBConcrete) ListCategoriesIDs(ctx context.Context) ([]int16, error) { - rows, err := svc.SQLDB.QueryContext(ctx, `SELECT id FROM categories`) - if err != nil { - return nil, err - } - defer rows.Close() - - var categories []int16 - for rows.Next() { - var category int16 - if err := rows.Scan(&category); err != nil { - return nil, err - } - categories = append(categories, category) - } - return categories, rows.Err() -} - -func (svc DBConcrete) Migrate() { - if err := db.Migrate(svc.SQLDB); err != nil { - panic(err) - } -} - -func (svc DBConcrete) CloseConnection() { - err := svc.SQLDB.Close() - if err != nil { - log.Fatalf("Error Closing DB Connection: %s", err) - } -} - -func (svc DBConcrete) AssertSuccessfulConnection(ctx context.Context) { - if err := svc.SQLDB.PingContext(ctx); err != nil { - panic(fmt.Errorf("db: cannot connect: %w", err)) - } -} diff --git a/cms/main.go b/cms/main.go index 1d7ecc7..7648f2a 100644 --- a/cms/main.go +++ b/cms/main.go @@ -6,6 +6,8 @@ import ( "log" "net/http" "os" + "thamanyah/cms/v2/internal/db" + "thamanyah/cms/v2/internal/db/repositories" "thamanyah/cms/v2/internal/handlers" "thamanyah/cms/v2/internal/services" @@ -32,9 +34,9 @@ func main() { } func runMigrate() { - concreteDBClient := services.CreateConcreteDbClient(requireDBConnectionString()) - defer concreteDBClient.CloseConnection() - concreteDBClient.Migrate() + concreteDBClient := db.CreateDBConnection(requireDBConnectionString()) + defer db.CloseConnection(concreteDBClient) + db.Migrate(concreteDBClient) log.Println("migrations applied successfully") } @@ -79,10 +81,15 @@ func runServer() { OutputBucket: mediaConvertOutputBucket, } - concreteDBClient := services.CreateConcreteDbClient(requireDBConnectionString()) - defer concreteDBClient.CloseConnection() - concreteDBClient.AssertSuccessfulConnection(context.Background()) - services.DB = concreteDBClient + concreteDBClient := db.CreateDBConnection(requireDBConnectionString()) + defer db.CloseConnection(concreteDBClient) + db.AssertSuccessfulConnection(context.Background(), concreteDBClient) + repositories.VideoRepo = &repositories.ConcreteVideoRepository{ + SQLDB: concreteDBClient, + } + repositories.CatagoriesRepo = &repositories.ConcreteCatagoriesRepository{ + SQLDB: concreteDBClient, + } mux := http.NewServeMux()