diff --git a/category.go b/category.go index 2f5a76e..abc780b 100644 --- a/category.go +++ b/category.go @@ -1,17 +1,18 @@ package main type Category struct { - Id int32 `json:"id"` - Name string `json:"name"` + ID int64 `json:"id"` + Name string `json:"name"` + Subcategories []Subcategory `json:"subcategories"` } type Subcategory struct { - Id int32 `json:"id"` + ID int64 `json:"id"` Name string `json:"name"` - CategoryId int32 `json:"categoryId"` + CategoryId int64 `json:"categoryId"` } -func getAllCategories() []Category { +func getAllCategories() (categories []Category, ok bool) { db := getSQLConnection() defer db.Close() @@ -19,22 +20,121 @@ func getAllCategories() []Category { rows, err := db.Query("SELECT * FROM category;") if err != nil { - panic(err) + //TODO: log this error + return nil, false + //panic(err) } defer rows.Close() - var categories []Category - for rows.Next() { var category Category - if err = rows.Scan(&category.Id, &category.Name); err != nil { + if err = rows.Scan(&category.ID, &category.Name); err != nil { println(err.Error()) } + subcategories, ok := getSubcategories(category.ID) + + if ok { + category.Subcategories = subcategories + } + categories = append(categories, category) } - return categories + return categories, true +} + +func insertNewCategory(name string) (recordID int32, ok bool) { + db := getSQLConnection() + + defer db.Close() + + err := db.QueryRow("INSERT INTO category (category_name) VALUES ($1) RETURNING category_id;", name).Scan(&recordID) + + if err != nil { + //TODO: log this error + println(err.Error()) + return 0, false + //panic(err) + } + + return recordID, true +} + +func deleteCategory(id int64) bool { + db := getSQLConnection() + + defer db.Close() + + _, err := db.Exec("DELETE FROM category WHERE category_id = $1;", id) + //TODO: log this if an error happened + if err != nil { + println(err.Error()) + return false + } + + return true +} + +func getCategoryByID(id int64) (category Category, ok bool) { + db := getSQLConnection() + + defer db.Close() + + if err := db.QueryRow("SELECT * FROM category WHERE category_id = $1;", id).Scan(&category.ID, &category.Name); err != nil { + return category, false + } + + subcategories, ok := getSubcategories(category.ID) + + if !ok { + //TODO: do something about not getting the subcategories, for now ignore. + return category, true + } + + category.Subcategories = subcategories + + return category, true +} + +// func getSubcategoryByID(id int64) (subcategory Subcategory, ok bool) { +// db := getSQLConnection() + +// defer db.Close() + +// if err := db.QueryRow("SELECT * FROM subcategory WHERE subcategory_id = $1;", id).Scan(&subcategory.ID, &subcategory.Name, &subcategory.CategoryId); err != nil { +// return subcategory, false +// } + +// return subcategory, true +// } + +func getSubcategories(categoryID int64) (subcategories []Subcategory, ok bool) { + db := getSQLConnection() + + defer db.Close() + + rows, err := db.Query("SELECT * FROM subcategory WHERE category_id = $1;", categoryID) + + if err != nil { + //TODO: log this error + return nil, false + //panic(err) + } + + defer rows.Close() + + for rows.Next() { + var subcategory Subcategory + + if err = rows.Scan(&subcategory.ID, &subcategory.Name, &subcategory.CategoryId); err != nil { + println(err.Error()) + } + + subcategories = append(subcategories, subcategory) + } + + return subcategories, true } diff --git a/database.go b/database.go index 671fb7d..bc497ab 100644 --- a/database.go +++ b/database.go @@ -7,6 +7,8 @@ import ( _ "github.com/lib/pq" ) +//TODO: maybe we can do the whole databaase connection things on application start and panic right away +//if we can't connect to the database. func getSQLConnection() *sql.DB { //Note: The postgres driver seems to get confused when no password is supplied, so omit it in the connection sting. //https://rajyavardhan.medium.com/when-you-get-relation-does-not-exist-in-postgres-7ffb0c3c674b diff --git a/go.mod b/go.mod index 04bd140..5fe7446 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module copyrightcrusader.org/inv go 1.16 -require github.com/lib/pq v1.10.2 // indirect +require github.com/lib/pq v1.10.2 diff --git a/main.go b/main.go index 0628953..7adeaf9 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,18 @@ package main import ( - "database/sql" "encoding/json" "fmt" "io/ioutil" "net/http" "strconv" + "strings" "time" ) //Go Best practices: https://github.com/golovers/effective-go type Item struct { - Id int32 `json:"id"` + ID int32 `json:"id"` Name int32 `json:"name"` ValueCents int32 `json:"valueCents"` PurchaseCostCents int32 `json:"purchasedCostCents"` @@ -27,8 +27,11 @@ type Item struct { func main() { http.HandleFunc("/categories", categories) - http.HandleFunc("/category", category) - err := http.ListenAndServe(":8080", nil) + //Note: seems adding a trailing slash allows routing URLs like /category/ID# + //No need for query parameters. + http.HandleFunc("/category/", category) + + err := http.ListenAndServe(":8088", nil) if err != nil { panic(err) @@ -39,7 +42,7 @@ func main() { func categories(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - getCategories(w, r) + getCategoriesHandler(w, r) default: w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("Method not allowed")) @@ -49,19 +52,19 @@ func categories(w http.ResponseWriter, r *http.Request) { func category(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - getCategory(w, r) + getCategoryHandler(w, r) case "POST": - insertCategory(w, r) + insertCategoryHandler(w, r) case "DELETE": - deleteCategory(w, r) + deleteCategoryHandler(w, r) default: w.WriteHeader(http.StatusMethodNotAllowed) w.Write([]byte("Method not allowed")) } } -func getCategory(w http.ResponseWriter, r *http.Request) { - id := r.URL.Query().Get("id") +func getCategoryHandler(w http.ResponseWriter, r *http.Request) { + id := strings.TrimPrefix(r.URL.Path, "/category/") value, err := strconv.ParseInt(id, 0, 64) @@ -71,19 +74,11 @@ func getCategory(w http.ResponseWriter, r *http.Request) { return } - db := getSQLConnection() + category, ok := getCategoryByID(value) - defer db.Close() - - var category Category - - if err := db.QueryRow("SELECT * FROM category WHERE category_id = $1;", value).Scan(&category.Id, &category.Name); err != nil { - if err == sql.ErrNoRows { - w.Write([]byte("No rows found")) - return - } - - w.Write([]byte(err.Error())) + if !ok { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to get category.")) return } @@ -99,8 +94,8 @@ func getCategory(w http.ResponseWriter, r *http.Request) { w.Write(jsonBytes) } -func deleteCategory(w http.ResponseWriter, r *http.Request) { - id := r.URL.Query().Get("id") +func deleteCategoryHandler(w http.ResponseWriter, r *http.Request) { + id := strings.TrimPrefix(r.URL.Path, "/category/") value, err := strconv.ParseInt(id, 0, 64) @@ -110,24 +105,23 @@ func deleteCategory(w http.ResponseWriter, r *http.Request) { return } - db := getSQLConnection() - - defer db.Close() - - _, err = db.Exec("DELETE FROM category WHERE category_id = $1;", value) - - if err != nil { + if !deleteCategory(value) { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + w.Write([]byte(fmt.Sprintf("Failed to delete %d", value))) return } w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("Deleted category '%d'.", value))) } -func getCategories(w http.ResponseWriter, r *http.Request) { - categories := getAllCategories() +func getCategoriesHandler(w http.ResponseWriter, r *http.Request) { + categories, ok := getAllCategories() + + if !ok { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Failed to fetch a list of categories.")) + return + } jsonBytes, err := json.Marshal(categories) @@ -141,12 +135,12 @@ func getCategories(w http.ResponseWriter, r *http.Request) { w.Write(jsonBytes) } -func insertCategory(w http.ResponseWriter, r *http.Request) { +func insertCategoryHandler(w http.ResponseWriter, r *http.Request) { ct := r.Header.Get("content-type") if ct != "application/json" { w.WriteHeader(http.StatusUnsupportedMediaType) - w.Write([]byte(fmt.Sprintf("Unsupported media type '%s'", ct))) + w.Write([]byte(fmt.Sprintf("Unsupported media type '%s', expected application/json", ct))) return } @@ -170,17 +164,11 @@ func insertCategory(w http.ResponseWriter, r *http.Request) { return } - db := getSQLConnection() + id, ok := insertNewCategory(category.Name) - defer db.Close() - - id := 0 - - err = db.QueryRow("INSERT INTO category (category_name) VALUES ($1) RETURNING category_id;", category.Name).Scan(&id) - - if err != nil { + if !ok { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + w.Write([]byte("Failed to create the new category.")) return }