From 195708adf6a0ad8069c0bd9885efc82374aa92cd Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 5 May 2026 20:32:03 -0500 Subject: [PATCH 01/14] Refactor platform and product handling structures - Consolidate and migrate API handlers for `platform` and `product` into their respective internal packages for improved modularity. - Replace `db` references with query-specific instances and interfaces. - Update imports, tests, and routes to reflect refactored handler structure. - Regenerate SQLC files and adjust configurations for modular placement. --- api/platform/platform.go | 13 ---- api/product/product.go | 13 ---- internal/db/platform/querier.go | 19 ----- internal/db/product/querier.go | 19 ----- internal/db/store.go | 14 ---- {api => internal}/platform/create.go | 7 +- .../{db/platform/db.go => platform/db.gen.go} | 0 internal/platform/db.go | 32 ++++++++ {api => internal}/platform/delete.go | 4 +- {api => internal}/platform/get.go | 9 +-- internal/platform/handler.go | 26 +++++++ .../platform/handler_test.go | 75 +++++++++---------- .../models.go => platform/models.gen.go} | 0 internal/platform/models.go | 42 +++++++++++ {queries => internal/platform}/platforms.sql | 0 .../platforms.sql.gen.go} | 0 {api => internal}/platform/update.go | 9 +-- {api => internal}/product/create.go | 7 +- .../{db/product/db.go => product/db.gen.go} | 0 {api => internal}/product/delete.go | 4 +- {api => internal}/product/get.go | 9 +-- internal/product/handler.go | 26 +++++++ .../product/handler_test.go | 55 +++++++------- .../models.go => product/models.gen.go} | 0 {queries => internal/product}/products.sql | 0 .../products.sql.gen.go} | 0 {api => internal}/product/update.go | 7 +- router/router.go | 18 ++--- sqlc.yml | 18 +++-- 29 files changed, 231 insertions(+), 195 deletions(-) delete mode 100644 api/platform/platform.go delete mode 100644 api/product/product.go delete mode 100644 internal/db/platform/querier.go delete mode 100644 internal/db/product/querier.go rename {api => internal}/platform/create.go (81%) rename internal/{db/platform/db.go => platform/db.gen.go} (100%) create mode 100644 internal/platform/db.go rename {api => internal}/platform/delete.go (88%) rename {api => internal}/platform/get.go (86%) create mode 100644 internal/platform/handler.go rename api/platform/platform_test.go => internal/platform/handler_test.go (85%) rename internal/{db/platform/models.go => platform/models.gen.go} (100%) create mode 100644 internal/platform/models.go rename {queries => internal/platform}/platforms.sql (100%) rename internal/{db/platform/platforms.sql.go => platform/platforms.sql.gen.go} (100%) rename {api => internal}/platform/update.go (83%) rename {api => internal}/product/create.go (87%) rename internal/{db/product/db.go => product/db.gen.go} (100%) rename {api => internal}/product/delete.go (88%) rename {api => internal}/product/get.go (88%) create mode 100644 internal/product/handler.go rename api/product/product_test.go => internal/product/handler_test.go (89%) rename internal/{db/product/models.go => product/models.gen.go} (100%) rename {queries => internal/product}/products.sql (100%) rename internal/{db/product/products.sql.go => product/products.sql.gen.go} (100%) rename {api => internal}/product/update.go (90%) diff --git a/api/platform/platform.go b/api/platform/platform.go deleted file mode 100644 index 6a870b3..0000000 --- a/api/platform/platform.go +++ /dev/null @@ -1,13 +0,0 @@ -package platformHandler - -import "products/internal/db/platform" - -type PlatformHandler struct { - queries platform.Querier -} - -func NewPlatformHandler(q platform.Querier) *PlatformHandler { - return &PlatformHandler{ - queries: q, - } -} diff --git a/api/product/product.go b/api/product/product.go deleted file mode 100644 index 42cc8dc..0000000 --- a/api/product/product.go +++ /dev/null @@ -1,13 +0,0 @@ -package productHandler - -import "products/internal/db/product" - -type ProductHandler struct { - queries product.Querier -} - -func NewProductHandler(q product.Querier) *ProductHandler { - return &ProductHandler{ - queries: q, - } -} diff --git a/internal/db/platform/querier.go b/internal/db/platform/querier.go deleted file mode 100644 index 0f6d185..0000000 --- a/internal/db/platform/querier.go +++ /dev/null @@ -1,19 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.30.0 - -package platform - -import ( - "context" -) - -type Querier interface { - CreatePlatform(ctx context.Context, arg CreatePlatformParams) error - DeletePlatform(ctx context.Context, id int32) (int32, error) - GetPlatform(ctx context.Context, id int32) (Platform, error) - GetPlatforms(ctx context.Context) ([]Platform, error) - UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (int32, error) -} - -var _ Querier = (*Queries)(nil) diff --git a/internal/db/product/querier.go b/internal/db/product/querier.go deleted file mode 100644 index d2646a8..0000000 --- a/internal/db/product/querier.go +++ /dev/null @@ -1,19 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.30.0 - -package product - -import ( - "context" -) - -type Querier interface { - CreateProduct(ctx context.Context, arg CreateProductParams) error - DeleteProduct(ctx context.Context, id int32) (int32, error) - GetProductById(ctx context.Context, id int32) (Product, error) - GetProductsByPlatform(ctx context.Context, platformID int32) ([]Product, error) - UpdateProduct(ctx context.Context, arg UpdateProductParams) (int32, error) -} - -var _ Querier = (*Queries)(nil) diff --git a/internal/db/store.go b/internal/db/store.go index ba0b06d..84df096 100644 --- a/internal/db/store.go +++ b/internal/db/store.go @@ -2,25 +2,11 @@ package db import ( "context" - "products/internal/db/platform" - "products/internal/db/product" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" ) -type Store struct { - Platform platform.Querier - Product product.Querier -} - -func New(db DBTX) *Store { - return &Store{ - Platform: platform.New(db), - Product: product.New(db), - } -} - // This interface is repeated in every db package, it is auto-generated by sqlc type DBTX interface { Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) diff --git a/api/platform/create.go b/internal/platform/create.go similarity index 81% rename from api/platform/create.go rename to internal/platform/create.go index db2c3a4..e1d0f41 100644 --- a/api/platform/create.go +++ b/internal/platform/create.go @@ -1,10 +1,9 @@ -package platformHandler +package platform import ( "context" "encoding/json" "net/http" - db "products/internal/db/platform" "time" "github.com/jackc/pgx/v5/pgtype" @@ -15,7 +14,7 @@ type CreatePlatformRequest struct { Description string `json:"description"` } -func (h *PlatformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) { +func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) { var req CreatePlatformRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) @@ -28,7 +27,7 @@ func (h *PlatformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - if err := h.queries.CreatePlatform(contextWithTimeOut, db.CreatePlatformParams{ + if err := h.queries.CreatePlatform(contextWithTimeOut, CreatePlatformParams{ Name: req.Name, Description: pgtype.Text{ Valid: req.Description != "", diff --git a/internal/db/platform/db.go b/internal/platform/db.gen.go similarity index 100% rename from internal/db/platform/db.go rename to internal/platform/db.gen.go diff --git a/internal/platform/db.go b/internal/platform/db.go new file mode 100644 index 0000000..855ef6e --- /dev/null +++ b/internal/platform/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package platform + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/api/platform/delete.go b/internal/platform/delete.go similarity index 88% rename from api/platform/delete.go rename to internal/platform/delete.go index 53f6ec6..687c3a4 100644 --- a/api/platform/delete.go +++ b/internal/platform/delete.go @@ -1,4 +1,4 @@ -package platformHandler +package platform import ( "context" @@ -10,7 +10,7 @@ import ( "github.com/jackc/pgx/v5" ) -func (h *PlatformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) { +func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { http.Error(w, "Invalid platform ID", http.StatusBadRequest) diff --git a/api/platform/get.go b/internal/platform/get.go similarity index 86% rename from api/platform/get.go rename to internal/platform/get.go index 40f18c8..93d2d3d 100644 --- a/api/platform/get.go +++ b/internal/platform/get.go @@ -1,4 +1,4 @@ -package platformHandler +package platform import ( "context" @@ -6,13 +6,12 @@ import ( "errors" "net/http" "products/internal" - db "products/internal/db/platform" "time" "github.com/jackc/pgx/v5" ) -func (h *PlatformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { +func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() platforms, err := h.queries.GetPlatforms(contextWithTimeOut) @@ -21,7 +20,7 @@ func (h *PlatformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { return } if platforms == nil { - platforms = []db.Platform{} + platforms = []Platform{} } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(platforms) @@ -32,7 +31,7 @@ func (h *PlatformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { } -func (h *PlatformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { +func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { http.Error(w, "Invalid platform ID", http.StatusBadRequest) diff --git a/internal/platform/handler.go b/internal/platform/handler.go new file mode 100644 index 0000000..4f4feef --- /dev/null +++ b/internal/platform/handler.go @@ -0,0 +1,26 @@ +package platform + +import ( + "net/http" +) + +type platformHandler struct { + queries *Queries +} + +func NewPlatformHandler(db DBTX) Handler { + queries := &Queries{ + db: db, + } + return &platformHandler{ + queries: queries, + } +} + +type Handler interface { + CreatePlatform(w http.ResponseWriter, r *http.Request) + GetPlatforms(w http.ResponseWriter, r *http.Request) + GetPlatform(w http.ResponseWriter, r *http.Request) + UpdatePlatform(w http.ResponseWriter, r *http.Request) + DeletePlatform(w http.ResponseWriter, r *http.Request) +} diff --git a/api/platform/platform_test.go b/internal/platform/handler_test.go similarity index 85% rename from api/platform/platform_test.go rename to internal/platform/handler_test.go index 33ec606..3214cda 100644 --- a/api/platform/platform_test.go +++ b/internal/platform/handler_test.go @@ -1,4 +1,4 @@ -package platformHandler +package platform import ( "bytes" @@ -7,7 +7,6 @@ import ( "errors" "net/http" "net/http/httptest" - "products/internal/db/platform" "strconv" "testing" @@ -17,14 +16,14 @@ import ( type mockPlatformQuerier struct { err error - createPlatform func(ctx context.Context, arg platform.CreatePlatformParams) error - getPlatforms func(ctx context.Context) ([]platform.Platform, error) - getPlatform func(ctx context.Context, id int32) (platform.Platform, error) + createPlatform func(ctx context.Context, arg CreatePlatformParams) error + getPlatforms func(ctx context.Context) ([]Platform, error) + getPlatform func(ctx context.Context, id int32) (Platform, error) deletePlatform func(ctx context.Context, id int32) (int32, error) - updatePlatform func(ctx context.Context, arg platform.UpdatePlatformParams) (int32, error) + updatePlatform func(ctx context.Context, arg UpdatePlatformParams) (int32, error) } -func (m *mockPlatformQuerier) CreatePlatform(ctx context.Context, arg platform.CreatePlatformParams) error { +func (m *mockPlatformQuerier) CreatePlatform(ctx context.Context, arg CreatePlatformParams) error { if m.createPlatform != nil { return m.createPlatform(ctx, arg) } @@ -38,21 +37,21 @@ func (m *mockPlatformQuerier) DeletePlatform(ctx context.Context, id int32) (int return -1, m.err } -func (m *mockPlatformQuerier) GetPlatform(ctx context.Context, id int32) (platform.Platform, error) { +func (m *mockPlatformQuerier) GetPlatform(ctx context.Context, id int32) (Platform, error) { if m.getPlatform != nil { return m.getPlatform(ctx, id) } - return platform.Platform{}, m.err + return Platform{}, m.err } -func (m *mockPlatformQuerier) GetPlatforms(ctx context.Context) ([]platform.Platform, error) { +func (m *mockPlatformQuerier) GetPlatforms(ctx context.Context) ([]Platform, error) { if m.getPlatforms != nil { return m.getPlatforms(ctx) } return nil, m.err } -func (m *mockPlatformQuerier) UpdatePlatform(ctx context.Context, arg platform.UpdatePlatformParams) (int32, error) { +func (m *mockPlatformQuerier) UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (int32, error) { if m.updatePlatform != nil { return m.updatePlatform(ctx, arg) } @@ -129,12 +128,12 @@ func TestGetPlatforms(t *testing.T) { tests := []struct { name string dbErr error - platforms []platform.Platform + platforms []Platform expectedStatus int }{ { name: "Success", - platforms: []platform.Platform{ + platforms: []Platform{ {ID: 1, Name: "Platform 1", Description: pgtype.Text{String: "Desc 1", Valid: true}}, {ID: 2, Name: "Platform 2", Description: pgtype.Text{String: "Desc 2", Valid: true}}, }, @@ -143,7 +142,7 @@ func TestGetPlatforms(t *testing.T) { }, { name: "Empty Success", - platforms: []platform.Platform{}, + platforms: []Platform{}, dbErr: nil, expectedStatus: http.StatusOK, }, @@ -159,7 +158,7 @@ func TestGetPlatforms(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mDB := &mockPlatformQuerier{ err: tt.dbErr, - getPlatforms: func(ctx context.Context) ([]platform.Platform, error) { + getPlatforms: func(ctx context.Context) ([]Platform, error) { return tt.platforms, tt.dbErr }, } @@ -175,7 +174,7 @@ func TestGetPlatforms(t *testing.T) { } if tt.expectedStatus == http.StatusOK { - var got []platform.Platform + var got []Platform err := json.Unmarshal(rr.Body.Bytes(), &got) if err != nil { t.Fatalf("failed to unmarshal response: %v", err) @@ -192,56 +191,56 @@ func TestGetPlatform(t *testing.T) { tests := []struct { name string id string - dbPlatform platform.Platform + dbPlatform Platform dbErr error expectedStatus int }{ { name: "Success", id: "1", - dbPlatform: platform.Platform{ID: 1, Name: "Platform 1"}, + dbPlatform: Platform{ID: 1, Name: "Platform 1"}, dbErr: nil, expectedStatus: http.StatusOK, }, { name: "Invalid ID", id: "abc", - dbPlatform: platform.Platform{}, + dbPlatform: Platform{}, dbErr: nil, expectedStatus: http.StatusBadRequest, }, { name: "Invalid ID (Zero)", id: "0", - dbPlatform: platform.Platform{}, + dbPlatform: Platform{}, dbErr: nil, expectedStatus: http.StatusBadRequest, }, { name: "Invalid ID (Negative)", id: "-1", - dbPlatform: platform.Platform{}, + dbPlatform: Platform{}, dbErr: nil, expectedStatus: http.StatusBadRequest, }, { name: "Invalid ID (Overflow)", id: "2147483648", - dbPlatform: platform.Platform{}, + dbPlatform: Platform{}, dbErr: nil, expectedStatus: http.StatusBadRequest, }, { name: "Not Found", id: "999", - dbPlatform: platform.Platform{}, + dbPlatform: Platform{}, dbErr: pgx.ErrNoRows, expectedStatus: http.StatusNotFound, }, { name: "DB Error", id: "1", - dbPlatform: platform.Platform{}, + dbPlatform: Platform{}, dbErr: errors.New("db error"), expectedStatus: http.StatusInternalServerError, }, @@ -251,7 +250,7 @@ func TestGetPlatform(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mDB := &mockPlatformQuerier{ err: tt.dbErr, - getPlatform: func(ctx context.Context, id int32) (platform.Platform, error) { + getPlatform: func(ctx context.Context, id int32) (Platform, error) { return tt.dbPlatform, tt.dbErr }, } @@ -268,12 +267,12 @@ func TestGetPlatform(t *testing.T) { } if tt.expectedStatus == http.StatusOK { - var got platform.Platform + var got Platform err := json.Unmarshal(rr.Body.Bytes(), &got) if err != nil { t.Fatalf("failed to unmarshal response: %v", err) } - if got.ID != tt.dbPlatform.ID || got.Name != tt.dbPlatform.Name { + if got.ID != tt.dbID || got.Name != tt.dbName { t.Errorf("expected platform %+v, got %+v", tt.dbPlatform, got) } } @@ -369,7 +368,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "Success", pathID: "1", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 1, Name: "Updated Platform", Description: pgtype.Text{String: "Updated Description", Valid: true}, @@ -380,7 +379,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "Invalid Path ID", pathID: "abc", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 1, Name: "Updated Platform", }, @@ -390,7 +389,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "Invalid Path ID (Zero)", pathID: "0", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 0, Name: "Updated Platform", }, @@ -400,7 +399,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "Invalid Path ID (Negative)", pathID: "-1", - requestBody: platform.Platform{ + requestBody: Platform{ ID: -1, Name: "Updated Platform", }, @@ -410,7 +409,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "Invalid Path ID (Overflow)", pathID: "2147483648", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 1, Name: "Updated Platform", }, @@ -420,7 +419,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "ID Mismatch", pathID: "2", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 1, Name: "Updated Platform", }, @@ -430,7 +429,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "Missing Name", pathID: "1", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 1, Name: "", }, @@ -447,7 +446,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "Platform Not Found", pathID: "999", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 999, Name: "Non-existent", }, @@ -457,7 +456,7 @@ func TestUpdatePlatform(t *testing.T) { { name: "DB Error", pathID: "1", - requestBody: platform.Platform{ + requestBody: Platform{ ID: 1, Name: "Test Platform", }, @@ -470,9 +469,9 @@ func TestUpdatePlatform(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mDB := &mockPlatformQuerier{ err: tt.dbErr, - updatePlatform: func(ctx context.Context, arg platform.UpdatePlatformParams) (int32, error) { + updatePlatform: func(ctx context.Context, arg UpdatePlatformParams) (int32, error) { if tt.name == "Success" { - expectedBody := tt.requestBody.(platform.Platform) + expectedBody := tt.requestBody.(Platform) if arg.ID != expectedBody.ID { t.Errorf("expected ID %d, got %d", expectedBody.ID, arg.ID) } diff --git a/internal/db/platform/models.go b/internal/platform/models.gen.go similarity index 100% rename from internal/db/platform/models.go rename to internal/platform/models.gen.go diff --git a/internal/platform/models.go b/internal/platform/models.go new file mode 100644 index 0000000..9fa4abc --- /dev/null +++ b/internal/platform/models.go @@ -0,0 +1,42 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package platform + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Flow struct { + ID int32 + ProductID int32 + Name string + Description pgtype.Text + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type FlowStep struct { + ID int32 + FlowID int32 + Current pgtype.UUID + Next pgtype.UUID +} + +type Platform struct { + ID int32 + Name string + Description pgtype.Text + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type Product struct { + ID int32 + PlatformID int32 + Name string + Description pgtype.Text + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} diff --git a/queries/platforms.sql b/internal/platform/platforms.sql similarity index 100% rename from queries/platforms.sql rename to internal/platform/platforms.sql diff --git a/internal/db/platform/platforms.sql.go b/internal/platform/platforms.sql.gen.go similarity index 100% rename from internal/db/platform/platforms.sql.go rename to internal/platform/platforms.sql.gen.go diff --git a/api/platform/update.go b/internal/platform/update.go similarity index 83% rename from api/platform/update.go rename to internal/platform/update.go index f4f9272..96a9431 100644 --- a/api/platform/update.go +++ b/internal/platform/update.go @@ -1,4 +1,4 @@ -package platformHandler +package platform import ( "context" @@ -6,15 +6,14 @@ import ( "errors" "net/http" "products/internal" - db "products/internal/db/platform" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) -func (h *PlatformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { - var req db.Platform +func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { + var req Platform if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return @@ -36,7 +35,7 @@ func (h *PlatformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - _, err := h.queries.UpdatePlatform(contextWithTimeOut, db.UpdatePlatformParams{ + _, err := h.queries.UpdatePlatform(contextWithTimeOut, UpdatePlatformParams{ ID: req.ID, Name: req.Name, Description: req.Description, diff --git a/api/product/create.go b/internal/product/create.go similarity index 87% rename from api/product/create.go rename to internal/product/create.go index fdd8388..d4c19e0 100644 --- a/api/product/create.go +++ b/internal/product/create.go @@ -1,10 +1,9 @@ -package productHandler +package product import ( "context" "encoding/json" "net/http" - "products/internal/db/product" "time" "github.com/jackc/pgx/v5/pgtype" @@ -16,7 +15,7 @@ type CreateProductRequest struct { Description string `json:"description"` } -func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { +func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { var req CreateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) @@ -27,7 +26,7 @@ func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { return } - params := product.CreateProductParams{ + params := CreateProductParams{ Name: req.Name, PlatformID: req.PlatformID, Description: pgtype.Text{ diff --git a/internal/db/product/db.go b/internal/product/db.gen.go similarity index 100% rename from internal/db/product/db.go rename to internal/product/db.gen.go diff --git a/api/product/delete.go b/internal/product/delete.go similarity index 88% rename from api/product/delete.go rename to internal/product/delete.go index d209b60..545f63c 100644 --- a/api/product/delete.go +++ b/internal/product/delete.go @@ -1,4 +1,4 @@ -package productHandler +package product import ( "context" @@ -10,7 +10,7 @@ import ( "github.com/jackc/pgx/v5" ) -func (h *ProductHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { +func (h *productHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { http.Error(w, "Invalid product ID", http.StatusBadRequest) diff --git a/api/product/get.go b/internal/product/get.go similarity index 88% rename from api/product/get.go rename to internal/product/get.go index 6bdd321..f7893c6 100644 --- a/api/product/get.go +++ b/internal/product/get.go @@ -1,4 +1,4 @@ -package productHandler +package product import ( "context" @@ -6,14 +6,13 @@ import ( "errors" "net/http" "products/internal" - db "products/internal/db/product" "time" "github.com/jackc/pgx/v5" ) // GetProductsByPlatform fetches products by platform ID. -func (h *ProductHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { +func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { platformID, ok := internal.GetIntFromRequestPath("platform_id", r) if !ok { http.Error(w, "Invalid platform ID", http.StatusBadRequest) @@ -30,7 +29,7 @@ func (h *ProductHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re } if products == nil { - products = []db.Product{} + products = []Product{} } w.Header().Set("Content-Type", "application/json") @@ -41,7 +40,7 @@ func (h *ProductHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re } // GetProductById fetches a single product by ID. -func (h *ProductHandler) GetProductById(w http.ResponseWriter, r *http.Request) { +func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { http.Error(w, "Invalid product ID", http.StatusBadRequest) diff --git a/internal/product/handler.go b/internal/product/handler.go new file mode 100644 index 0000000..6c0bcbf --- /dev/null +++ b/internal/product/handler.go @@ -0,0 +1,26 @@ +package product + +import ( + "net/http" +) + +type productHandler struct { + queries *Queries +} + +func NewProductHandler(db DBTX) Handler { + queries := &Queries{ + db: db, + } + return &productHandler{ + queries: queries, + } +} + +type Handler interface { + CreateProduct(w http.ResponseWriter, r *http.Request) + GetProductsByPlatform(w http.ResponseWriter, r *http.Request) + GetProductById(w http.ResponseWriter, r *http.Request) + UpdateProduct(w http.ResponseWriter, r *http.Request) + DeleteProduct(w http.ResponseWriter, r *http.Request) +} diff --git a/api/product/product_test.go b/internal/product/handler_test.go similarity index 89% rename from api/product/product_test.go rename to internal/product/handler_test.go index 4a36a1e..3dc5c68 100644 --- a/api/product/product_test.go +++ b/internal/product/handler_test.go @@ -1,4 +1,4 @@ -package productHandler +package product import ( "bytes" @@ -7,7 +7,6 @@ import ( "errors" "net/http" "net/http/httptest" - "products/internal/db/product" "testing" "time" @@ -15,14 +14,14 @@ import ( ) type mockProductQuerier struct { - createProductFunc func(ctx context.Context, arg product.CreateProductParams) error + createProductFunc func(ctx context.Context, arg CreateProductParams) error deleteProductFunc func(ctx context.Context, id int32) (int32, error) - getProductByIdFunc func(ctx context.Context, id int32) (product.Product, error) - getProductsByPlatformFunc func(ctx context.Context, platformID int32) ([]product.Product, error) - updateProductFunc func(ctx context.Context, arg product.UpdateProductParams) (int32, error) + getProductByIdFunc func(ctx context.Context, id int32) (Product, error) + getProductsByPlatformFunc func(ctx context.Context, platformID int32) ([]Product, error) + updateProductFunc func(ctx context.Context, arg UpdateProductParams) (int32, error) } -func (m *mockProductQuerier) CreateProduct(ctx context.Context, arg product.CreateProductParams) error { +func (m *mockProductQuerier) CreateProduct(ctx context.Context, arg CreateProductParams) error { return m.createProductFunc(ctx, arg) } @@ -30,15 +29,15 @@ func (m *mockProductQuerier) DeleteProduct(ctx context.Context, id int32) (int32 return m.deleteProductFunc(ctx, id) } -func (m *mockProductQuerier) GetProductById(ctx context.Context, id int32) (product.Product, error) { +func (m *mockProductQuerier) GetProductById(ctx context.Context, id int32) (Product, error) { return m.getProductByIdFunc(ctx, id) } -func (m *mockProductQuerier) GetProductsByPlatform(ctx context.Context, platformID int32) ([]product.Product, error) { +func (m *mockProductQuerier) GetProductsByPlatform(ctx context.Context, platformID int32) ([]Product, error) { return m.getProductsByPlatformFunc(ctx, platformID) } -func (m *mockProductQuerier) UpdateProduct(ctx context.Context, arg product.UpdateProductParams) (int32, error) { +func (m *mockProductQuerier) UpdateProduct(ctx context.Context, arg UpdateProductParams) (int32, error) { return m.updateProductFunc(ctx, arg) } @@ -56,7 +55,7 @@ func TestCreateProduct(t *testing.T) { Name: "Test Product", }, mockSetup: func(m *mockProductQuerier) { - m.createProductFunc = func(ctx context.Context, arg product.CreateProductParams) error { + m.createProductFunc = func(ctx context.Context, arg CreateProductParams) error { if arg.Name != "Test Product" { return errors.New("unexpected name") } @@ -81,7 +80,7 @@ func TestCreateProduct(t *testing.T) { Name: "Fail Product", }, mockSetup: func(m *mockProductQuerier) { - m.createProductFunc = func(ctx context.Context, arg product.CreateProductParams) error { + m.createProductFunc = func(ctx context.Context, arg CreateProductParams) error { return errors.New("db error") } }, @@ -243,11 +242,11 @@ func TestGetProductsByPlatform(t *testing.T) { name: "Success", platformID: "1", mockSetup: func(m *mockProductQuerier) { - m.getProductsByPlatformFunc = func(ctx context.Context, platformID int32) ([]product.Product, error) { + m.getProductsByPlatformFunc = func(ctx context.Context, platformID int32) ([]Product, error) { if platformID != 1 { return nil, errors.New("unexpected platform id") } - return []product.Product{ + return []Product{ {ID: 1, PlatformID: 1, Name: "Product 1"}, {ID: 2, PlatformID: 1, Name: "Product 2"}, }, nil @@ -260,7 +259,7 @@ func TestGetProductsByPlatform(t *testing.T) { name: "Empty list (Nil guard)", platformID: "1", mockSetup: func(m *mockProductQuerier) { - m.getProductsByPlatformFunc = func(ctx context.Context, platformID int32) ([]product.Product, error) { + m.getProductsByPlatformFunc = func(ctx context.Context, platformID int32) ([]Product, error) { return nil, nil } }, @@ -277,7 +276,7 @@ func TestGetProductsByPlatform(t *testing.T) { name: "DB Failure", platformID: "1", mockSetup: func(m *mockProductQuerier) { - m.getProductsByPlatformFunc = func(ctx context.Context, platformID int32) ([]product.Product, error) { + m.getProductsByPlatformFunc = func(ctx context.Context, platformID int32) ([]Product, error) { return nil, errors.New("db error") } }, @@ -302,7 +301,7 @@ func TestGetProductsByPlatform(t *testing.T) { } if tt.expectedStatus == http.StatusOK { - var products []product.Product + var products []Product if err := json.NewDecoder(rr.Body).Decode(&products); err != nil { t.Fatalf("failed to decode response: %v", err) } @@ -328,11 +327,11 @@ func TestGetProductById(t *testing.T) { name: "Success", id: "1", mockSetup: func(m *mockProductQuerier) { - m.getProductByIdFunc = func(ctx context.Context, id int32) (product.Product, error) { + m.getProductByIdFunc = func(ctx context.Context, id int32) (Product, error) { if id != 1 { - return product.Product{}, errors.New("unexpected id") + return Product{}, errors.New("unexpected id") } - return product.Product{ID: 1, Name: "Test Product"}, nil + return Product{ID: 1, Name: "Test Product"}, nil } }, expectedStatus: http.StatusOK, @@ -341,8 +340,8 @@ func TestGetProductById(t *testing.T) { name: "Not Found", id: "999", mockSetup: func(m *mockProductQuerier) { - m.getProductByIdFunc = func(ctx context.Context, id int32) (product.Product, error) { - return product.Product{}, pgx.ErrNoRows + m.getProductByIdFunc = func(ctx context.Context, id int32) (Product, error) { + return Product{}, pgx.ErrNoRows } }, expectedStatus: http.StatusNotFound, @@ -357,8 +356,8 @@ func TestGetProductById(t *testing.T) { name: "DB Failure", id: "1", mockSetup: func(m *mockProductQuerier) { - m.getProductByIdFunc = func(ctx context.Context, id int32) (product.Product, error) { - return product.Product{}, errors.New("db error") + m.getProductByIdFunc = func(ctx context.Context, id int32) (Product, error) { + return Product{}, errors.New("db error") } }, expectedStatus: http.StatusInternalServerError, @@ -407,7 +406,7 @@ func TestUpdateProduct(t *testing.T) { Description: "Updated Description", }, mockSetup: func(m *mockProductQuerier) { - m.updateProductFunc = func(ctx context.Context, arg product.UpdateProductParams) (int32, error) { + m.updateProductFunc = func(ctx context.Context, arg UpdateProductParams) (int32, error) { if arg.ID != 1 { return 0, errors.New("unexpected id") } @@ -486,7 +485,7 @@ func TestUpdateProduct(t *testing.T) { Name: "Test", }, mockSetup: func(m *mockProductQuerier) { - m.updateProductFunc = func(ctx context.Context, arg product.UpdateProductParams) (int32, error) { + m.updateProductFunc = func(ctx context.Context, arg UpdateProductParams) (int32, error) { deadline, ok := ctx.Deadline() if !ok { return 0, errors.New("deadline not set") @@ -508,7 +507,7 @@ func TestUpdateProduct(t *testing.T) { Name: "Fail", }, mockSetup: func(m *mockProductQuerier) { - m.updateProductFunc = func(ctx context.Context, arg product.UpdateProductParams) (int32, error) { + m.updateProductFunc = func(ctx context.Context, arg UpdateProductParams) (int32, error) { return 0, errors.New("db error") } }, @@ -522,7 +521,7 @@ func TestUpdateProduct(t *testing.T) { Name: "Not Found", }, mockSetup: func(m *mockProductQuerier) { - m.updateProductFunc = func(ctx context.Context, arg product.UpdateProductParams) (int32, error) { + m.updateProductFunc = func(ctx context.Context, arg UpdateProductParams) (int32, error) { return 0, pgx.ErrNoRows } }, diff --git a/internal/db/product/models.go b/internal/product/models.gen.go similarity index 100% rename from internal/db/product/models.go rename to internal/product/models.gen.go diff --git a/queries/products.sql b/internal/product/products.sql similarity index 100% rename from queries/products.sql rename to internal/product/products.sql diff --git a/internal/db/product/products.sql.go b/internal/product/products.sql.gen.go similarity index 100% rename from internal/db/product/products.sql.go rename to internal/product/products.sql.gen.go diff --git a/api/product/update.go b/internal/product/update.go similarity index 90% rename from api/product/update.go rename to internal/product/update.go index a686b5b..111d681 100644 --- a/api/product/update.go +++ b/internal/product/update.go @@ -1,4 +1,4 @@ -package productHandler +package product import ( "context" @@ -6,7 +6,6 @@ import ( "errors" "net/http" "products/internal" - "products/internal/db/product" "strings" "time" @@ -20,7 +19,7 @@ type UpdateProductRequest struct { Description string `json:"description"` } -func (h *ProductHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { +func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { http.Error(w, "Invalid product ID", http.StatusBadRequest) @@ -39,7 +38,7 @@ func (h *ProductHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { return } - params := product.UpdateProductParams{ + params := UpdateProductParams{ ID: id, PlatformID: req.PlatformID, Name: req.Name, diff --git a/router/router.go b/router/router.go index a8390bb..873888f 100644 --- a/router/router.go +++ b/router/router.go @@ -3,13 +3,11 @@ package router import ( "log/slog" "net/http" - platformHandler "products/api/platform" - productHandler "products/api/product" systemHandler "products/api/system" "products/internal" "products/internal/db" - platformDb "products/internal/db/platform" - productDb "products/internal/db/product" + "products/internal/platform" + "products/internal/product" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -20,8 +18,6 @@ func SetupRouter(dbConn db.DBTX) http.Handler { slog.Debug("Setting up router") router := chi.NewRouter() - store := db.New(dbConn) - router.Use(internal.StructuredLogger(slog.Default())) router.Use(middleware.Recoverer) router.Use(middleware.Compress(5)) @@ -34,8 +30,8 @@ func SetupRouter(dbConn db.DBTX) http.Handler { })) registerSystemCallHandler(router) - registerPlatformCallHandler(store.Platform, router) - registerProductCallHandler(store.Product, router) + registerPlatformCallHandler(platform.NewPlatformHandler(dbConn), router) + registerProductCallHandler(product.NewProductHandler(dbConn), router) slog.Debug("Router setup complete") return router } @@ -45,8 +41,7 @@ func registerSystemCallHandler(r *chi.Mux) { r.Get("/api/time", h.GetTime) } -func registerPlatformCallHandler(q platformDb.Querier, r *chi.Mux) { - handler := platformHandler.NewPlatformHandler(q) +func registerPlatformCallHandler(handler platform.Handler, r *chi.Mux) { r.Route("/api/platforms", func(u chi.Router) { u.Post("/", handler.CreatePlatform) u.Get("/", handler.GetPlatforms) @@ -56,8 +51,7 @@ func registerPlatformCallHandler(q platformDb.Querier, r *chi.Mux) { }) } -func registerProductCallHandler(q productDb.Querier, r *chi.Mux) { - handler := productHandler.NewProductHandler(q) +func registerProductCallHandler(handler product.Handler, r *chi.Mux) { r.Route("/api/products", func(u chi.Router) { u.Post("/", handler.CreateProduct) u.Get("/{id}", handler.GetProductById) diff --git a/sqlc.yml b/sqlc.yml index ad67b35..0a7a89c 100644 --- a/sqlc.yml +++ b/sqlc.yml @@ -1,21 +1,23 @@ version: "2" sql: - engine: "postgresql" - queries: "queries/platforms.sql" + queries: "internal/platform/platforms.sql" schema: "schema.sql" gen: go: - out: "internal/db/platform" + out: "internal/platform" sql_package: "pgx/v5" - output_models_file_name: "models.go" - emit_interface: true + output_models_file_name: "models.gen.go" + output_db_file_name: "db.gen.go" + output_files_suffix: ".gen.go" - engine: "postgresql" - queries: "queries/products.sql" + queries: "internal/product/products.sql" schema: "schema.sql" gen: go: - out: "internal/db/product" + out: "internal/product" sql_package: "pgx/v5" - output_models_file_name: "models.go" - emit_interface: true \ No newline at end of file + output_models_file_name: "models.gen.go" + output_db_file_name: "db.gen.go" + output_files_suffix: ".gen.go" From de661c8974ef4c04dcd0667f1ff545f56c5d921f Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 5 May 2026 20:40:43 -0500 Subject: [PATCH 02/14] Generate Querier interfaces for `platform` and `product`, refactor handlers to use interfaces, and update SQLC configuration for interface output. --- internal/platform/handler.go | 2 +- internal/platform/querier.gen.go | 19 +++++++++++++++++++ internal/product/handler.go | 2 +- internal/product/querier.gen.go | 19 +++++++++++++++++++ sqlc.yml | 4 ++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 internal/platform/querier.gen.go create mode 100644 internal/product/querier.gen.go diff --git a/internal/platform/handler.go b/internal/platform/handler.go index 4f4feef..af16d40 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -5,7 +5,7 @@ import ( ) type platformHandler struct { - queries *Queries + queries Querier } func NewPlatformHandler(db DBTX) Handler { diff --git a/internal/platform/querier.gen.go b/internal/platform/querier.gen.go new file mode 100644 index 0000000..0f6d185 --- /dev/null +++ b/internal/platform/querier.gen.go @@ -0,0 +1,19 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package platform + +import ( + "context" +) + +type Querier interface { + CreatePlatform(ctx context.Context, arg CreatePlatformParams) error + DeletePlatform(ctx context.Context, id int32) (int32, error) + GetPlatform(ctx context.Context, id int32) (Platform, error) + GetPlatforms(ctx context.Context) ([]Platform, error) + UpdatePlatform(ctx context.Context, arg UpdatePlatformParams) (int32, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/internal/product/handler.go b/internal/product/handler.go index 6c0bcbf..55e54d3 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -5,7 +5,7 @@ import ( ) type productHandler struct { - queries *Queries + queries Querier } func NewProductHandler(db DBTX) Handler { diff --git a/internal/product/querier.gen.go b/internal/product/querier.gen.go new file mode 100644 index 0000000..d2646a8 --- /dev/null +++ b/internal/product/querier.gen.go @@ -0,0 +1,19 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package product + +import ( + "context" +) + +type Querier interface { + CreateProduct(ctx context.Context, arg CreateProductParams) error + DeleteProduct(ctx context.Context, id int32) (int32, error) + GetProductById(ctx context.Context, id int32) (Product, error) + GetProductsByPlatform(ctx context.Context, platformID int32) ([]Product, error) + UpdateProduct(ctx context.Context, arg UpdateProductParams) (int32, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/sqlc.yml b/sqlc.yml index 0a7a89c..5d7b476 100644 --- a/sqlc.yml +++ b/sqlc.yml @@ -10,6 +10,8 @@ sql: output_models_file_name: "models.gen.go" output_db_file_name: "db.gen.go" output_files_suffix: ".gen.go" + output_querier_file_name: "querier.gen.go" + emit_interface: true - engine: "postgresql" queries: "internal/product/products.sql" @@ -21,3 +23,5 @@ sql: output_models_file_name: "models.gen.go" output_db_file_name: "db.gen.go" output_files_suffix: ".gen.go" + output_querier_file_name: "querier.gen.go" + emit_interface: true From 38e475d561e0a115cfa7dc24f3dcd835a4ed0ae6 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 5 May 2026 20:45:15 -0500 Subject: [PATCH 03/14] Remove redundant `DBTX` interface from `store.go` --- internal/db/store.go | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 internal/db/store.go diff --git a/internal/db/store.go b/internal/db/store.go deleted file mode 100644 index 84df096..0000000 --- a/internal/db/store.go +++ /dev/null @@ -1,15 +0,0 @@ -package db - -import ( - "context" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" -) - -// This interface is repeated in every db package, it is auto-generated by sqlc -type DBTX interface { - Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) - Query(context.Context, string, ...interface{}) (pgx.Rows, error) - QueryRow(context.Context, string, ...interface{}) pgx.Row -} From 269fb51e20702a5694629d06813e324e9c64ddde Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 5 May 2026 20:49:08 -0500 Subject: [PATCH 04/14] Refactor DB connection handling: remove redundant `DBTX` interface and streamline connection closure --- main.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 4ae3f5d..ba870e8 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( "time" internalConfig "products/internal/config" - "products/internal/db" "products/router" "github.com/jackc/pgx/v5/pgxpool" @@ -28,10 +27,7 @@ func main() { if err != nil { log.Fatal(err) } - - if dbPool, ok := dbConn.(*pgxpool.Pool); ok { - defer dbPool.Close() - } + defer dbConn.Close() r := router.SetupRouter(dbConn) addr := internalConfig.GetConfigValue("ADDRESS") @@ -57,7 +53,7 @@ func main() { } -func getDbConn() (db.DBTX, error) { +func getDbConn() (*pgxpool.Pool, error) { connStr, err := getConnStr() if err != nil { return nil, err From a9978fcb36228ddc4275833e55ace49c4fa6c96a Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 5 May 2026 20:49:35 -0500 Subject: [PATCH 05/14] Revert "Remove redundant `DBTX` interface from `store.go`" This reverts commit 38e475d561e0a115cfa7dc24f3dcd835a4ed0ae6. --- internal/db/store.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 internal/db/store.go diff --git a/internal/db/store.go b/internal/db/store.go new file mode 100644 index 0000000..84df096 --- /dev/null +++ b/internal/db/store.go @@ -0,0 +1,15 @@ +package db + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// This interface is repeated in every db package, it is auto-generated by sqlc +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} From 10f7baca018104c9d7ef6963269cebdb566641d6 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 5 May 2026 20:52:39 -0500 Subject: [PATCH 06/14] Remove SQLC-generated files and refactor handlers to use concrete `platformHandler` and `productHandler` structs --- internal/platform/db.go | 32 ----------------------- internal/platform/handler_test.go | 12 ++++----- internal/platform/models.go | 42 ------------------------------- internal/product/handler_test.go | 10 ++++---- 4 files changed, 11 insertions(+), 85 deletions(-) delete mode 100644 internal/platform/db.go delete mode 100644 internal/platform/models.go diff --git a/internal/platform/db.go b/internal/platform/db.go deleted file mode 100644 index 855ef6e..0000000 --- a/internal/platform/db.go +++ /dev/null @@ -1,32 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.30.0 - -package platform - -import ( - "context" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" -) - -type DBTX interface { - Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) - Query(context.Context, string, ...interface{}) (pgx.Rows, error) - QueryRow(context.Context, string, ...interface{}) pgx.Row -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -type Queries struct { - db DBTX -} - -func (q *Queries) WithTx(tx pgx.Tx) *Queries { - return &Queries{ - db: tx, - } -} diff --git a/internal/platform/handler_test.go b/internal/platform/handler_test.go index 3214cda..44e7222 100644 --- a/internal/platform/handler_test.go +++ b/internal/platform/handler_test.go @@ -103,7 +103,7 @@ func TestCreatePlatform(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mDB := &mockPlatformQuerier{err: tt.dbErr} - h := NewPlatformHandler(mDB) + h := &platformHandler{queries: mDB} var body []byte if s, ok := tt.requestBody.(string); ok { @@ -162,7 +162,7 @@ func TestGetPlatforms(t *testing.T) { return tt.platforms, tt.dbErr }, } - h := NewPlatformHandler(mDB) + h := &platformHandler{queries: mDB} req := httptest.NewRequest(http.MethodGet, "/api/platforms", nil) rr := httptest.NewRecorder() @@ -254,7 +254,7 @@ func TestGetPlatform(t *testing.T) { return tt.dbPlatform, tt.dbErr }, } - h := NewPlatformHandler(mDB) + h := &platformHandler{queries: mDB} req := httptest.NewRequest(http.MethodGet, "/api/platforms/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -272,7 +272,7 @@ func TestGetPlatform(t *testing.T) { if err != nil { t.Fatalf("failed to unmarshal response: %v", err) } - if got.ID != tt.dbID || got.Name != tt.dbName { + if got.ID != tt.dbPlatform.ID || got.Name != tt.dbPlatform.Name { t.Errorf("expected platform %+v, got %+v", tt.dbPlatform, got) } } @@ -342,7 +342,7 @@ func TestDeletePlatform(t *testing.T) { return -1, tt.dbErr }, } - h := NewPlatformHandler(mDB) + h := &platformHandler{queries: mDB} req := httptest.NewRequest(http.MethodDelete, "/api/platforms/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -491,7 +491,7 @@ func TestUpdatePlatform(t *testing.T) { return arg.ID, tt.dbErr }, } - h := NewPlatformHandler(mDB) + h := &platformHandler{queries: mDB} var body []byte if s, ok := tt.requestBody.(string); ok { diff --git a/internal/platform/models.go b/internal/platform/models.go deleted file mode 100644 index 9fa4abc..0000000 --- a/internal/platform/models.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.30.0 - -package platform - -import ( - "github.com/jackc/pgx/v5/pgtype" -) - -type Flow struct { - ID int32 - ProductID int32 - Name string - Description pgtype.Text - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz -} - -type FlowStep struct { - ID int32 - FlowID int32 - Current pgtype.UUID - Next pgtype.UUID -} - -type Platform struct { - ID int32 - Name string - Description pgtype.Text - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz -} - -type Product struct { - ID int32 - PlatformID int32 - Name string - Description pgtype.Text - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz -} diff --git a/internal/product/handler_test.go b/internal/product/handler_test.go index 3dc5c68..febf93b 100644 --- a/internal/product/handler_test.go +++ b/internal/product/handler_test.go @@ -108,7 +108,7 @@ func TestCreateProduct(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductQuerier{} tt.mockSetup(mock) - h := NewProductHandler(mock) + h := &productHandler{queries: mock} var body []byte if s, ok := tt.requestBody.(string); ok { @@ -215,7 +215,7 @@ func TestDeleteProduct(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductQuerier{} tt.mockSetup(mock) - h := NewProductHandler(mock) + h := &productHandler{queries: mock} req := httptest.NewRequest(http.MethodDelete, "/api/products/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -288,7 +288,7 @@ func TestGetProductsByPlatform(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductQuerier{} tt.mockSetup(mock) - h := NewProductHandler(mock) + h := &productHandler{queries: mock} req := httptest.NewRequest(http.MethodGet, "/api/platforms/"+tt.platformID+"/products", nil) req.SetPathValue("platform_id", tt.platformID) @@ -368,7 +368,7 @@ func TestGetProductById(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductQuerier{} tt.mockSetup(mock) - h := NewProductHandler(mock) + h := &productHandler{queries: mock} req := httptest.NewRequest(http.MethodGet, "/api/products/"+tt.id, nil) req.SetPathValue("id", tt.id) @@ -533,7 +533,7 @@ func TestUpdateProduct(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mock := &mockProductQuerier{} tt.mockSetup(mock) - h := NewProductHandler(mock) + h := &productHandler{queries: mock} var body []byte if s, ok := tt.requestBody.(string); ok { From b5adb227b37500794b923a9a41dc439aa8bc5da0 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 19:07:26 -0500 Subject: [PATCH 07/14] Restore and reimplement `platform` handlers with improved struct definition and request validation --- internal/platform/create.go | 46 --------- internal/platform/delete.go | 32 ------ internal/platform/get.go | 58 ----------- internal/platform/handler.go | 162 +++++++++++++++++++++++++++++- internal/platform/handler_test.go | 6 +- internal/platform/update.go | 57 ----------- 6 files changed, 161 insertions(+), 200 deletions(-) delete mode 100644 internal/platform/create.go delete mode 100644 internal/platform/delete.go delete mode 100644 internal/platform/get.go delete mode 100644 internal/platform/update.go diff --git a/internal/platform/create.go b/internal/platform/create.go deleted file mode 100644 index e1d0f41..0000000 --- a/internal/platform/create.go +++ /dev/null @@ -1,46 +0,0 @@ -package platform - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "github.com/jackc/pgx/v5/pgtype" -) - -type CreatePlatformRequest struct { - Name string `json:"name"` - Description string `json:"description"` -} - -func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) { - var req CreatePlatformRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - if req.Name == "" { - http.Error(w, "Name is required", http.StatusBadRequest) - return - } - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - if err := h.queries.CreatePlatform(contextWithTimeOut, CreatePlatformParams{ - Name: req.Name, - Description: pgtype.Text{ - Valid: req.Description != "", - String: req.Description, - }, - Timestamp: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - }); err != nil { - http.Error(w, "Failed to create platform", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) -} diff --git a/internal/platform/delete.go b/internal/platform/delete.go deleted file mode 100644 index 687c3a4..0000000 --- a/internal/platform/delete.go +++ /dev/null @@ -1,32 +0,0 @@ -package platform - -import ( - "context" - "errors" - "net/http" - "products/internal" - "time" - - "github.com/jackc/pgx/v5" -) - -func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) { - id, ok := internal.GetIntFromRequestPath("id", r) - if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) - return - } - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - _, err := h.queries.DeletePlatform(contextWithTimeOut, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Platform not found", http.StatusNotFound) - return - } - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/platform/get.go b/internal/platform/get.go deleted file mode 100644 index 93d2d3d..0000000 --- a/internal/platform/get.go +++ /dev/null @@ -1,58 +0,0 @@ -package platform - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "products/internal" - "time" - - "github.com/jackc/pgx/v5" -) - -func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - platforms, err := h.queries.GetPlatforms(contextWithTimeOut) - if err != nil { - http.Error(w, "Failed to fetch platforms", http.StatusInternalServerError) - return - } - if platforms == nil { - platforms = []Platform{} - } - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(platforms) - if err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } - -} - -func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { - id, ok := internal.GetIntFromRequestPath("id", r) - if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) - return - } - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - platform, err := h.queries.GetPlatform(contextWithTimeOut, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Platform not found", http.StatusNotFound) - return - } - http.Error(w, "Failed to fetch platform", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(platform) - if err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } -} diff --git a/internal/platform/handler.go b/internal/platform/handler.go index af16d40..9276152 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -1,12 +1,16 @@ package platform import ( + "context" + "encoding/json" + "errors" "net/http" -) + "products/internal" + "time" -type platformHandler struct { - queries Querier -} + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) func NewPlatformHandler(db DBTX) Handler { queries := &Queries{ @@ -24,3 +28,153 @@ type Handler interface { UpdatePlatform(w http.ResponseWriter, r *http.Request) DeletePlatform(w http.ResponseWriter, r *http.Request) } +type createPlatformRequest struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type platformHandler struct { + queries Querier +} + +func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) { + var req createPlatformRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if req.Name == "" { + http.Error(w, "Name is required", http.StatusBadRequest) + return + } + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + if err := h.queries.CreatePlatform(contextWithTimeOut, CreatePlatformParams{ + Name: req.Name, + Description: pgtype.Text{ + Valid: req.Description != "", + String: req.Description, + }, + Timestamp: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + }); err != nil { + http.Error(w, "Failed to create platform", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + +func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { + var req Platform + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + id, ok := internal.GetIntFromRequestPath("id", r) + if !ok { + http.Error(w, "Invalid platform ID", http.StatusBadRequest) + return + } + + if req.Name == "" { + http.Error(w, "Name is required", http.StatusBadRequest) + return + } + + if req.ID != id { + http.Error(w, "Platform ID does not match path", http.StatusBadRequest) + return + } + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + _, err := h.queries.UpdatePlatform(contextWithTimeOut, UpdatePlatformParams{ + ID: req.ID, + Name: req.Name, + Description: req.Description, + Updatedat: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + http.Error(w, "Platform not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to update platform", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) { + id, ok := internal.GetIntFromRequestPath("id", r) + if !ok { + http.Error(w, "Invalid platform ID", http.StatusBadRequest) + return + } + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + _, err := h.queries.DeletePlatform(contextWithTimeOut, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + http.Error(w, "Platform not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + platforms, err := h.queries.GetPlatforms(contextWithTimeOut) + if err != nil { + http.Error(w, "Failed to fetch platforms", http.StatusInternalServerError) + return + } + if platforms == nil { + platforms = []Platform{} + } + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(platforms) + if err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } + +} + +func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { + id, ok := internal.GetIntFromRequestPath("id", r) + if !ok { + http.Error(w, "Invalid platform ID", http.StatusBadRequest) + return + } + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + platform, err := h.queries.GetPlatform(contextWithTimeOut, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + http.Error(w, "Platform not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to fetch platform", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(platform) + if err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } +} diff --git a/internal/platform/handler_test.go b/internal/platform/handler_test.go index 44e7222..f0555e5 100644 --- a/internal/platform/handler_test.go +++ b/internal/platform/handler_test.go @@ -67,7 +67,7 @@ func TestCreatePlatform(t *testing.T) { }{ { name: "Success", - requestBody: CreatePlatformRequest{ + requestBody: createPlatformRequest{ Name: "Test Platform", Description: "Test Description", }, @@ -76,7 +76,7 @@ func TestCreatePlatform(t *testing.T) { }, { name: "Missing Name", - requestBody: CreatePlatformRequest{ + requestBody: createPlatformRequest{ Name: "", Description: "Test Description", }, @@ -91,7 +91,7 @@ func TestCreatePlatform(t *testing.T) { }, { name: "DB Error", - requestBody: CreatePlatformRequest{ + requestBody: createPlatformRequest{ Name: "Test Platform", Description: "Test Description", }, diff --git a/internal/platform/update.go b/internal/platform/update.go deleted file mode 100644 index 96a9431..0000000 --- a/internal/platform/update.go +++ /dev/null @@ -1,57 +0,0 @@ -package platform - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "products/internal" - "time" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" -) - -func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { - var req Platform - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - id, ok := internal.GetIntFromRequestPath("id", r) - if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) - return - } - - if req.Name == "" { - http.Error(w, "Name is required", http.StatusBadRequest) - return - } - - if req.ID != id { - http.Error(w, "Platform ID does not match path", http.StatusBadRequest) - return - } - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - _, err := h.queries.UpdatePlatform(contextWithTimeOut, UpdatePlatformParams{ - ID: req.ID, - Name: req.Name, - Description: req.Description, - Updatedat: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - }) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Platform not found", http.StatusNotFound) - return - } - http.Error(w, "Failed to update platform", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusNoContent) -} From 2c0a598879080cdd1d9c93833d4dacad4db81f72 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 19:10:36 -0500 Subject: [PATCH 08/14] Consolidate `product` handlers into a single file, refactor request structs, and improve test consistency --- internal/product/create.go | 51 --------- internal/product/delete.go | 30 ----- internal/product/get.go | 68 ----------- internal/product/handler.go | 186 ++++++++++++++++++++++++++++++- internal/product/handler_test.go | 28 ++--- internal/product/update.go | 68 ----------- 6 files changed, 196 insertions(+), 235 deletions(-) delete mode 100644 internal/product/create.go delete mode 100644 internal/product/delete.go delete mode 100644 internal/product/get.go delete mode 100644 internal/product/update.go diff --git a/internal/product/create.go b/internal/product/create.go deleted file mode 100644 index d4c19e0..0000000 --- a/internal/product/create.go +++ /dev/null @@ -1,51 +0,0 @@ -package product - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "github.com/jackc/pgx/v5/pgtype" -) - -type CreateProductRequest struct { - Name string `json:"name"` - PlatformID int32 `json:"platform_id"` - Description string `json:"description"` -} - -func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { - var req CreateProductRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - if req.Name == "" || req.PlatformID == 0 { - http.Error(w, "Name and platform ID are required", http.StatusBadRequest) - return - } - - params := CreateProductParams{ - Name: req.Name, - PlatformID: req.PlatformID, - Description: pgtype.Text{ - Valid: req.Description != "", - String: req.Description, - }, - Timestamp: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - } - - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - if err := h.queries.CreateProduct(contextWithTimeOut, params); err != nil { - http.Error(w, "Failed to create product", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) - -} diff --git a/internal/product/delete.go b/internal/product/delete.go deleted file mode 100644 index 545f63c..0000000 --- a/internal/product/delete.go +++ /dev/null @@ -1,30 +0,0 @@ -package product - -import ( - "context" - "errors" - "net/http" - "products/internal" - "time" - - "github.com/jackc/pgx/v5" -) - -func (h *productHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { - id, ok := internal.GetIntFromRequestPath("id", r) - if !ok { - http.Error(w, "Invalid product ID", http.StatusBadRequest) - return - } - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - if _, err := h.queries.DeleteProduct(contextWithTimeOut, id); err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Product not found", http.StatusNotFound) - return - } - http.Error(w, "Failed to delete product", http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/product/get.go b/internal/product/get.go deleted file mode 100644 index f7893c6..0000000 --- a/internal/product/get.go +++ /dev/null @@ -1,68 +0,0 @@ -package product - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "products/internal" - "time" - - "github.com/jackc/pgx/v5" -) - -// GetProductsByPlatform fetches products by platform ID. -func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { - platformID, ok := internal.GetIntFromRequestPath("platform_id", r) - if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) - return - } - - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - products, err := h.queries.GetProductsByPlatform(ctx, platformID) - if err != nil { - http.Error(w, "Failed to fetch products", http.StatusInternalServerError) - return - } - - if products == nil { - products = []Product{} - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(products); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } -} - -// GetProductById fetches a single product by ID. -func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) { - id, ok := internal.GetIntFromRequestPath("id", r) - if !ok { - http.Error(w, "Invalid product ID", http.StatusBadRequest) - return - } - - ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) - defer cancel() - - product, err := h.queries.GetProductById(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Product not found", http.StatusNotFound) - return - } - http.Error(w, "Failed to fetch product", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(product); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } -} diff --git a/internal/product/handler.go b/internal/product/handler.go index 55e54d3..39d4391 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -1,12 +1,17 @@ package product import ( + "context" + "encoding/json" + "errors" "net/http" -) + "products/internal" + "strings" + "time" -type productHandler struct { - queries Querier -} + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) func NewProductHandler(db DBTX) Handler { queries := &Queries{ @@ -24,3 +29,176 @@ type Handler interface { UpdateProduct(w http.ResponseWriter, r *http.Request) DeleteProduct(w http.ResponseWriter, r *http.Request) } + +type createProductRequest struct { + Name string `json:"name"` + PlatformID int32 `json:"platform_id"` + Description string `json:"description"` +} + +type updateProductRequest struct { + PlatformID int32 `json:"platform_id"` + Name string `json:"name"` + Description string `json:"description"` +} + +type productHandler struct { + queries Querier +} + +func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { + var req createProductRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + if req.Name == "" || req.PlatformID == 0 { + http.Error(w, "Name and platform ID are required", http.StatusBadRequest) + return + } + + params := CreateProductParams{ + Name: req.Name, + PlatformID: req.PlatformID, + Description: pgtype.Text{ + Valid: req.Description != "", + String: req.Description, + }, + Timestamp: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + } + + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + if err := h.queries.CreateProduct(contextWithTimeOut, params); err != nil { + http.Error(w, "Failed to create product", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + +func (h *productHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { + id, ok := internal.GetIntFromRequestPath("id", r) + if !ok { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + if _, err := h.queries.DeleteProduct(contextWithTimeOut, id); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + http.Error(w, "Product not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to delete product", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { + id, ok := internal.GetIntFromRequestPath("id", r) + if !ok { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + + var req updateProductRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + req.Name = strings.TrimSpace(req.Name) + if req.Name == "" || req.PlatformID == 0 { + http.Error(w, "Name and platform ID are required", http.StatusBadRequest) + return + } + + params := UpdateProductParams{ + ID: id, + PlatformID: req.PlatformID, + Name: req.Name, + Description: pgtype.Text{ + Valid: req.Description != "", + String: req.Description, + }, + UpdatedAt: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + } + + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + + if _, err := h.queries.UpdateProduct(ctx, params); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + http.Error(w, "Product not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to update product", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +// GetProductsByPlatform fetches products by platform ID. +func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { + platformID, ok := internal.GetIntFromRequestPath("platform_id", r) + if !ok { + http.Error(w, "Invalid platform ID", http.StatusBadRequest) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + products, err := h.queries.GetProductsByPlatform(ctx, platformID) + if err != nil { + http.Error(w, "Failed to fetch products", http.StatusInternalServerError) + return + } + + if products == nil { + products = []Product{} + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(products); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } +} + +// GetProductById fetches a single product by ID. +func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) { + id, ok := internal.GetIntFromRequestPath("id", r) + if !ok { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + product, err := h.queries.GetProductById(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + http.Error(w, "Product not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to fetch product", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(product); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } +} diff --git a/internal/product/handler_test.go b/internal/product/handler_test.go index febf93b..8542f85 100644 --- a/internal/product/handler_test.go +++ b/internal/product/handler_test.go @@ -50,7 +50,7 @@ func TestCreateProduct(t *testing.T) { }{ { name: "Success", - requestBody: CreateProductRequest{ + requestBody: createProductRequest{ PlatformID: 1, Name: "Test Product", }, @@ -75,7 +75,7 @@ func TestCreateProduct(t *testing.T) { }, { name: "DB Failure", - requestBody: CreateProductRequest{ + requestBody: createProductRequest{ PlatformID: 1, Name: "Fail Product", }, @@ -88,7 +88,7 @@ func TestCreateProduct(t *testing.T) { }, { name: "Missing Name", - requestBody: CreateProductRequest{ + requestBody: createProductRequest{ PlatformID: 1, }, mockSetup: func(m *mockProductQuerier) {}, @@ -96,7 +96,7 @@ func TestCreateProduct(t *testing.T) { }, { name: "Missing PlatformID", - requestBody: CreateProductRequest{ + requestBody: createProductRequest{ Name: "Test Product", }, mockSetup: func(m *mockProductQuerier) {}, @@ -400,7 +400,7 @@ func TestUpdateProduct(t *testing.T) { { name: "Success", id: "1", - requestBody: UpdateProductRequest{ + requestBody: updateProductRequest{ PlatformID: 2, Name: "Updated Product", Description: "Updated Description", @@ -424,7 +424,7 @@ func TestUpdateProduct(t *testing.T) { { name: "Invalid ID", id: "abc", - requestBody: UpdateProductRequest{PlatformID: 1, Name: "Test"}, + requestBody: updateProductRequest{PlatformID: 1, Name: "Test"}, mockSetup: func(m *mockProductQuerier) {}, expectedStatus: http.StatusBadRequest, }, @@ -438,7 +438,7 @@ func TestUpdateProduct(t *testing.T) { { name: "Missing Name", id: "1", - requestBody: UpdateProductRequest{ + requestBody: updateProductRequest{ PlatformID: 1, }, mockSetup: func(m *mockProductQuerier) {}, @@ -447,7 +447,7 @@ func TestUpdateProduct(t *testing.T) { { name: "Missing PlatformID", id: "1", - requestBody: UpdateProductRequest{ + requestBody: updateProductRequest{ Name: "Test", }, mockSetup: func(m *mockProductQuerier) {}, @@ -456,21 +456,21 @@ func TestUpdateProduct(t *testing.T) { { name: "Zero ID", id: "0", - requestBody: UpdateProductRequest{PlatformID: 1, Name: "Test"}, + requestBody: updateProductRequest{PlatformID: 1, Name: "Test"}, mockSetup: func(m *mockProductQuerier) {}, expectedStatus: http.StatusBadRequest, }, { name: "Negative ID", id: "-1", - requestBody: UpdateProductRequest{PlatformID: 1, Name: "Test"}, + requestBody: updateProductRequest{PlatformID: 1, Name: "Test"}, mockSetup: func(m *mockProductQuerier) {}, expectedStatus: http.StatusBadRequest, }, { name: "Whitespace Name", id: "1", - requestBody: UpdateProductRequest{ + requestBody: updateProductRequest{ PlatformID: 1, Name: " ", }, @@ -480,7 +480,7 @@ func TestUpdateProduct(t *testing.T) { { name: "Timeout verification", id: "1", - requestBody: UpdateProductRequest{ + requestBody: updateProductRequest{ PlatformID: 1, Name: "Test", }, @@ -502,7 +502,7 @@ func TestUpdateProduct(t *testing.T) { { name: "DB Failure", id: "1", - requestBody: UpdateProductRequest{ + requestBody: updateProductRequest{ PlatformID: 1, Name: "Fail", }, @@ -516,7 +516,7 @@ func TestUpdateProduct(t *testing.T) { { name: "Not Found", id: "999", - requestBody: UpdateProductRequest{ + requestBody: updateProductRequest{ PlatformID: 1, Name: "Not Found", }, diff --git a/internal/product/update.go b/internal/product/update.go deleted file mode 100644 index 111d681..0000000 --- a/internal/product/update.go +++ /dev/null @@ -1,68 +0,0 @@ -package product - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "products/internal" - "strings" - "time" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" -) - -type UpdateProductRequest struct { - PlatformID int32 `json:"platform_id"` - Name string `json:"name"` - Description string `json:"description"` -} - -func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { - id, ok := internal.GetIntFromRequestPath("id", r) - if !ok { - http.Error(w, "Invalid product ID", http.StatusBadRequest) - return - } - - var req UpdateProductRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - req.Name = strings.TrimSpace(req.Name) - if req.Name == "" || req.PlatformID == 0 { - http.Error(w, "Name and platform ID are required", http.StatusBadRequest) - return - } - - params := UpdateProductParams{ - ID: id, - PlatformID: req.PlatformID, - Name: req.Name, - Description: pgtype.Text{ - Valid: req.Description != "", - String: req.Description, - }, - UpdatedAt: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - } - - ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - - if _, err := h.queries.UpdateProduct(ctx, params); err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Product not found", http.StatusNotFound) - return - } - http.Error(w, "Failed to update product", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} From 541914873702edad936ca055d3c197257e08a10f Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 19:15:40 -0500 Subject: [PATCH 09/14] Extract `Handler` interface from `handler.go` to a new `interface.go` file for improved modularity in `product` package. --- internal/product/handler.go | 8 -------- internal/product/interface.go | 11 +++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 internal/product/interface.go diff --git a/internal/product/handler.go b/internal/product/handler.go index 39d4391..72c5424 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -22,14 +22,6 @@ func NewProductHandler(db DBTX) Handler { } } -type Handler interface { - CreateProduct(w http.ResponseWriter, r *http.Request) - GetProductsByPlatform(w http.ResponseWriter, r *http.Request) - GetProductById(w http.ResponseWriter, r *http.Request) - UpdateProduct(w http.ResponseWriter, r *http.Request) - DeleteProduct(w http.ResponseWriter, r *http.Request) -} - type createProductRequest struct { Name string `json:"name"` PlatformID int32 `json:"platform_id"` diff --git a/internal/product/interface.go b/internal/product/interface.go new file mode 100644 index 0000000..4ad54d9 --- /dev/null +++ b/internal/product/interface.go @@ -0,0 +1,11 @@ +package product + +import "net/http" + +type Handler interface { + CreateProduct(w http.ResponseWriter, r *http.Request) + GetProductsByPlatform(w http.ResponseWriter, r *http.Request) + GetProductById(w http.ResponseWriter, r *http.Request) + UpdateProduct(w http.ResponseWriter, r *http.Request) + DeleteProduct(w http.ResponseWriter, r *http.Request) +} From 8043cad96ff0203e45a6477ef1572ddddfc1c4bb Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 19:27:01 -0500 Subject: [PATCH 10/14] Refactor request handling for `product` and `platform`: extract request structs into dedicated files, implement `ToParams` methods, and add unit tests to improve modularity and test coverage. --- internal/platform/handler.go | 29 +-------- internal/platform/request.go | 47 ++++++++++++++ internal/platform/request_test.go | 96 +++++++++++++++++++++++++++ internal/product/handler.go | 45 +------------ internal/product/request.go | 50 ++++++++++++++ internal/product/request_test.go | 104 ++++++++++++++++++++++++++++++ 6 files changed, 303 insertions(+), 68 deletions(-) create mode 100644 internal/platform/request.go create mode 100644 internal/platform/request_test.go create mode 100644 internal/product/request.go create mode 100644 internal/product/request_test.go diff --git a/internal/platform/handler.go b/internal/platform/handler.go index 9276152..b3cb8e4 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -9,7 +9,6 @@ import ( "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" ) func NewPlatformHandler(db DBTX) Handler { @@ -28,10 +27,6 @@ type Handler interface { UpdatePlatform(w http.ResponseWriter, r *http.Request) DeletePlatform(w http.ResponseWriter, r *http.Request) } -type createPlatformRequest struct { - Name string `json:"name"` - Description string `json:"description"` -} type platformHandler struct { queries Querier @@ -50,17 +45,7 @@ func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - if err := h.queries.CreatePlatform(contextWithTimeOut, CreatePlatformParams{ - Name: req.Name, - Description: pgtype.Text{ - Valid: req.Description != "", - String: req.Description, - }, - Timestamp: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - }); err != nil { + if err := h.queries.CreatePlatform(contextWithTimeOut, req.ToParams()); err != nil { http.Error(w, "Failed to create platform", http.StatusInternalServerError) return } @@ -69,7 +54,7 @@ func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) } func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { - var req Platform + var req updatePlatformRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return @@ -91,15 +76,7 @@ func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - _, err := h.queries.UpdatePlatform(contextWithTimeOut, UpdatePlatformParams{ - ID: req.ID, - Name: req.Name, - Description: req.Description, - Updatedat: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - }) + _, err := h.queries.UpdatePlatform(contextWithTimeOut, req.ToParams(id)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { http.Error(w, "Platform not found", http.StatusNotFound) diff --git a/internal/platform/request.go b/internal/platform/request.go new file mode 100644 index 0000000..d24001e --- /dev/null +++ b/internal/platform/request.go @@ -0,0 +1,47 @@ +package platform + +import ( + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +type createPlatformRequest struct { + Name string `json:"name"` + Description string `json:"description"` +} + +func (r *createPlatformRequest) ToParams() CreatePlatformParams { + return CreatePlatformParams{ + Name: r.Name, + Description: pgtype.Text{ + Valid: r.Description != "", + String: r.Description, + }, + Timestamp: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + } +} + +type updatePlatformRequest struct { + ID int32 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +func (r *updatePlatformRequest) ToParams(id int32) UpdatePlatformParams { + return UpdatePlatformParams{ + ID: id, + Name: r.Name, + Description: pgtype.Text{ + Valid: r.Description != "", + String: r.Description, + }, + Updatedat: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + } +} diff --git a/internal/platform/request_test.go b/internal/platform/request_test.go new file mode 100644 index 0000000..b8a0cc0 --- /dev/null +++ b/internal/platform/request_test.go @@ -0,0 +1,96 @@ +package platform + +import ( + "testing" + "time" +) + +func TestCreatePlatformRequest_ToParams(t *testing.T) { + req := createPlatformRequest{ + Name: "Test Platform", + Description: "Test Description", + } + + params := req.ToParams() + + if params.Name != req.Name { + t.Errorf("expected name %q, got %q", req.Name, params.Name) + } + if !params.Description.Valid { + t.Error("expected description to be valid") + } + if params.Description.String != req.Description { + t.Errorf("expected description %q, got %q", req.Description, params.Description.String) + } + if !params.Timestamp.Valid { + t.Error("expected timestamp to be valid") + } + diff := time.Since(params.Timestamp.Time) + if diff < 0 { + diff = -diff + } + if diff > 2*time.Second { + t.Errorf("timestamp too far from now: %v", diff) + } +} + +func TestCreatePlatformRequest_ToParams_EmptyDescription(t *testing.T) { + req := createPlatformRequest{ + Name: "Test Platform", + Description: "", + } + + params := req.ToParams() + + if params.Description.Valid { + t.Error("expected description to be invalid") + } +} + +func TestUpdatePlatformRequest_ToParams(t *testing.T) { + req := updatePlatformRequest{ + ID: 1, + Name: "Updated Platform", + Description: "Updated Description", + } + id := int32(1) + + params := req.ToParams(id) + + if params.ID != id { + t.Errorf("expected id %d, got %d", id, params.ID) + } + if params.Name != req.Name { + t.Errorf("expected name %q, got %q", req.Name, params.Name) + } + if !params.Description.Valid { + t.Error("expected description to be valid") + } + if params.Description.String != req.Description { + t.Errorf("expected description %q, got %q", req.Description, params.Description.String) + } + if !params.Updatedat.Valid { + t.Error("expected updatedat to be valid") + } + diff := time.Since(params.Updatedat.Time) + if diff < 0 { + diff = -diff + } + if diff > 2*time.Second { + t.Errorf("updatedat too far from now: %v", diff) + } +} + +func TestUpdatePlatformRequest_ToParams_EmptyDescription(t *testing.T) { + req := updatePlatformRequest{ + ID: 1, + Name: "Updated Platform", + Description: "", + } + + params := req.ToParams(1) + + if params.Description.Valid { + t.Error("expected description to be invalid") + } +} diff --git a/internal/product/handler.go b/internal/product/handler.go index 72c5424..2c0b704 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -10,7 +10,6 @@ import ( "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" ) func NewProductHandler(db DBTX) Handler { @@ -22,18 +21,6 @@ func NewProductHandler(db DBTX) Handler { } } -type createProductRequest struct { - Name string `json:"name"` - PlatformID int32 `json:"platform_id"` - Description string `json:"description"` -} - -type updateProductRequest struct { - PlatformID int32 `json:"platform_id"` - Name string `json:"name"` - Description string `json:"description"` -} - type productHandler struct { queries Querier } @@ -49,22 +36,10 @@ func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { return } - params := CreateProductParams{ - Name: req.Name, - PlatformID: req.PlatformID, - Description: pgtype.Text{ - Valid: req.Description != "", - String: req.Description, - }, - Timestamp: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - } - contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - if err := h.queries.CreateProduct(contextWithTimeOut, params); err != nil { + + if err := h.queries.CreateProduct(contextWithTimeOut, req.ToParams()); err != nil { http.Error(w, "Failed to create product", http.StatusInternalServerError) return } @@ -110,24 +85,10 @@ func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { return } - params := UpdateProductParams{ - ID: id, - PlatformID: req.PlatformID, - Name: req.Name, - Description: pgtype.Text{ - Valid: req.Description != "", - String: req.Description, - }, - UpdatedAt: pgtype.Timestamptz{ - Valid: true, - Time: time.Now().UTC(), - }, - } - ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() - if _, err := h.queries.UpdateProduct(ctx, params); err != nil { + if _, err := h.queries.UpdateProduct(ctx, req.ToParams(id)); err != nil { if errors.Is(err, pgx.ErrNoRows) { http.Error(w, "Product not found", http.StatusNotFound) return diff --git a/internal/product/request.go b/internal/product/request.go new file mode 100644 index 0000000..e01ecdd --- /dev/null +++ b/internal/product/request.go @@ -0,0 +1,50 @@ +package product + +import ( + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +type createProductRequest struct { + Name string `json:"name"` + PlatformID int32 `json:"platform_id"` + Description string `json:"description"` +} + +func (r *createProductRequest) ToParams() CreateProductParams { + return CreateProductParams{ + Name: r.Name, + PlatformID: r.PlatformID, + Description: pgtype.Text{ + Valid: r.Description != "", + String: r.Description, + }, + Timestamp: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + } +} + +type updateProductRequest struct { + PlatformID int32 `json:"platform_id"` + Name string `json:"name"` + Description string `json:"description"` +} + +func (r *updateProductRequest) ToParams(id int32) UpdateProductParams { + return UpdateProductParams{ + ID: id, + PlatformID: r.PlatformID, + Name: r.Name, + Description: pgtype.Text{ + Valid: r.Description != "", + String: r.Description, + }, + UpdatedAt: pgtype.Timestamptz{ + Valid: true, + Time: time.Now().UTC(), + }, + } +} diff --git a/internal/product/request_test.go b/internal/product/request_test.go new file mode 100644 index 0000000..a210c16 --- /dev/null +++ b/internal/product/request_test.go @@ -0,0 +1,104 @@ +package product + +import ( + "testing" + "time" +) + +func TestCreateProductRequest_ToParams(t *testing.T) { + req := createProductRequest{ + Name: "Test Product", + PlatformID: 1, + Description: "Test Description", + } + + params := req.ToParams() + + if params.Name != req.Name { + t.Errorf("expected name %q, got %q", req.Name, params.Name) + } + if params.PlatformID != req.PlatformID { + t.Errorf("expected platform_id %d, got %d", req.PlatformID, params.PlatformID) + } + if !params.Description.Valid { + t.Error("expected description to be valid") + } + if params.Description.String != req.Description { + t.Errorf("expected description %q, got %q", req.Description, params.Description.String) + } + if !params.Timestamp.Valid { + t.Error("expected timestamp to be valid") + } + diff := time.Since(params.Timestamp.Time) + if diff < 0 { + diff = -diff + } + if diff > 2*time.Second { + t.Errorf("timestamp too far from now: %v", diff) + } +} + +func TestCreateProductRequest_ToParams_EmptyDescription(t *testing.T) { + req := createProductRequest{ + Name: "Test Product", + PlatformID: 1, + Description: "", + } + + params := req.ToParams() + + if params.Description.Valid { + t.Error("expected description to be invalid") + } +} + +func TestUpdateProductRequest_ToParams(t *testing.T) { + req := updateProductRequest{ + PlatformID: 2, + Name: "Updated Product", + Description: "Updated Description", + } + id := int32(10) + + params := req.ToParams(id) + + if params.ID != id { + t.Errorf("expected id %d, got %d", id, params.ID) + } + if params.PlatformID != req.PlatformID { + t.Errorf("expected platform_id %d, got %d", req.PlatformID, params.PlatformID) + } + if params.Name != req.Name { + t.Errorf("expected name %q, got %q", req.Name, params.Name) + } + if !params.Description.Valid { + t.Error("expected description to be valid") + } + if params.Description.String != req.Description { + t.Errorf("expected description %q, got %q", req.Description, params.Description.String) + } + if !params.UpdatedAt.Valid { + t.Error("expected updated_at to be valid") + } + diff := time.Since(params.UpdatedAt.Time) + if diff < 0 { + diff = -diff + } + if diff > 2*time.Second { + t.Errorf("updated_at too far from now: %v", diff) + } +} + +func TestUpdateProductRequest_ToParams_EmptyDescription(t *testing.T) { + req := updateProductRequest{ + PlatformID: 2, + Name: "Updated Product", + Description: "", + } + + params := req.ToParams(10) + + if params.Description.Valid { + t.Error("expected description to be invalid") + } +} From 8e137f46a36ea66526fed9092eb45d516cc6864b Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 19:29:54 -0500 Subject: [PATCH 11/14] Extract `Handler` interface from `handler.go` to a new `interface.go` file for improved modularity in `platform` package. --- internal/platform/handler.go | 8 -------- internal/platform/interface.go | 11 +++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 internal/platform/interface.go diff --git a/internal/platform/handler.go b/internal/platform/handler.go index b3cb8e4..f404e43 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -20,14 +20,6 @@ func NewPlatformHandler(db DBTX) Handler { } } -type Handler interface { - CreatePlatform(w http.ResponseWriter, r *http.Request) - GetPlatforms(w http.ResponseWriter, r *http.Request) - GetPlatform(w http.ResponseWriter, r *http.Request) - UpdatePlatform(w http.ResponseWriter, r *http.Request) - DeletePlatform(w http.ResponseWriter, r *http.Request) -} - type platformHandler struct { queries Querier } diff --git a/internal/platform/interface.go b/internal/platform/interface.go new file mode 100644 index 0000000..d528f74 --- /dev/null +++ b/internal/platform/interface.go @@ -0,0 +1,11 @@ +package platform + +import "net/http" + +type Handler interface { + CreatePlatform(w http.ResponseWriter, r *http.Request) + GetPlatforms(w http.ResponseWriter, r *http.Request) + GetPlatform(w http.ResponseWriter, r *http.Request) + UpdatePlatform(w http.ResponseWriter, r *http.Request) + DeletePlatform(w http.ResponseWriter, r *http.Request) +} From ad245ee15a08b18f053aa799144ce9319ebc6484 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 19:58:43 -0500 Subject: [PATCH 12/14] Improve error handling for `platform` deletion: add specific `pgx.ErrNoRows` handling, enhance test coverage, and log internal server errors --- internal/platform/handler.go | 4 +++- internal/platform/handler_test.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/platform/handler.go b/internal/platform/handler.go index f404e43..b84e35d 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -95,7 +95,9 @@ func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) http.Error(w, "Platform not found", http.StatusNotFound) return } - http.Error(w, err.Error(), http.StatusInternalServerError) + logger := internal.LoggerFromContext(r.Context()) + logger.Error("Failed to delete platform", "error", err, "platform_id", id) + http.Error(w, "Internal server error", http.StatusInternalServerError) return } diff --git a/internal/platform/handler_test.go b/internal/platform/handler_test.go index f0555e5..103627a 100644 --- a/internal/platform/handler_test.go +++ b/internal/platform/handler_test.go @@ -286,6 +286,7 @@ func TestDeletePlatform(t *testing.T) { id string dbErr error expectedStatus int + expectedBody string }{ { name: "Success", @@ -304,6 +305,14 @@ func TestDeletePlatform(t *testing.T) { id: "1", dbErr: errors.New("db error"), expectedStatus: http.StatusInternalServerError, + expectedBody: "Internal server error\n", + }, + { + name: "DB Error (pgx.ErrNoRows)", + id: "1", + dbErr: pgx.ErrNoRows, + expectedStatus: http.StatusNotFound, + expectedBody: "Platform not found\n", }, { name: "Invalid ID (Zero)", @@ -353,6 +362,12 @@ func TestDeletePlatform(t *testing.T) { if rr.Code != tt.expectedStatus { t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code) } + + if tt.expectedBody != "" { + if rr.Body.String() != tt.expectedBody { + t.Errorf("expected body %q, got %q", tt.expectedBody, rr.Body.String()) + } + } }) } } From 50d2964e582eda7ff9adf3eb604d645e47406b84 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 20:04:30 -0500 Subject: [PATCH 13/14] Trim whitespace from `Name` in product requests and add test case for validation --- internal/product/handler.go | 2 ++ internal/product/handler_test.go | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/internal/product/handler.go b/internal/product/handler.go index 2c0b704..95dbb8b 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -31,6 +31,8 @@ func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { http.Error(w, "Invalid request body", http.StatusBadRequest) return } + + req.Name = strings.TrimSpace(req.Name) if req.Name == "" || req.PlatformID == 0 { http.Error(w, "Name and platform ID are required", http.StatusBadRequest) return diff --git a/internal/product/handler_test.go b/internal/product/handler_test.go index 8542f85..24743b4 100644 --- a/internal/product/handler_test.go +++ b/internal/product/handler_test.go @@ -102,6 +102,15 @@ func TestCreateProduct(t *testing.T) { mockSetup: func(m *mockProductQuerier) {}, expectedStatus: http.StatusBadRequest, }, + { + name: "Whitespace Name", + requestBody: createProductRequest{ + PlatformID: 1, + Name: " ", + }, + mockSetup: func(m *mockProductQuerier) {}, + expectedStatus: http.StatusBadRequest, + }, } for _, tt := range tests { From 5214876b5a1b038ee7be36e196f87bb94fc34af4 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 6 May 2026 20:05:33 -0500 Subject: [PATCH 14/14] Add test for `ToParams` ensuring `ID` precedence in `updatePlatformRequest` --- internal/platform/request_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/platform/request_test.go b/internal/platform/request_test.go index b8a0cc0..58730a6 100644 --- a/internal/platform/request_test.go +++ b/internal/platform/request_test.go @@ -94,3 +94,27 @@ func TestUpdatePlatformRequest_ToParams_EmptyDescription(t *testing.T) { t.Error("expected description to be invalid") } } + +func TestUpdatePlatformRequest_ToParams_IDPrecedence(t *testing.T) { + req := updatePlatformRequest{ + ID: 100, // ID from body + Name: "Precedence Test", + Description: "Testing ID precedence", + } + pathID := int32(200) // ID from path/argument + + params := req.ToParams(pathID) + + // Verify that the method argument ID is preferred over the struct field ID + if params.ID != pathID { + t.Errorf("expected id %d (path ID), got %d", pathID, params.ID) + } + + // Verify that Name and Description are preserved + if params.Name != req.Name { + t.Errorf("expected name %q, got %q", req.Name, params.Name) + } + if params.Description.String != req.Description { + t.Errorf("expected description %q, got %q", req.Description, params.Description.String) + } +}