Category data now includes their subcategories. Categories can now be requested by ID in a prettier way.
This commit is contained in:
+110
-10
@@ -1,17 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
Id int32 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Subcategories []Subcategory `json:"subcategories"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subcategory struct {
|
type Subcategory struct {
|
||||||
Id int32 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CategoryId int32 `json:"categoryId"`
|
CategoryId int64 `json:"categoryId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllCategories() []Category {
|
func getAllCategories() (categories []Category, ok bool) {
|
||||||
db := getSQLConnection()
|
db := getSQLConnection()
|
||||||
|
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
@@ -19,22 +20,121 @@ func getAllCategories() []Category {
|
|||||||
rows, err := db.Query("SELECT * FROM category;")
|
rows, err := db.Query("SELECT * FROM category;")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
//TODO: log this error
|
||||||
|
return nil, false
|
||||||
|
//panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var categories []Category
|
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var category Category
|
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())
|
println(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subcategories, ok := getSubcategories(category.ID)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
category.Subcategories = subcategories
|
||||||
|
}
|
||||||
|
|
||||||
categories = append(categories, category)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
_ "github.com/lib/pq"
|
_ "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 {
|
func getSQLConnection() *sql.DB {
|
||||||
//Note: The postgres driver seems to get confused when no password is supplied, so omit it in the connection sting.
|
//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
|
//https://rajyavardhan.medium.com/when-you-get-relation-does-not-exist-in-postgres-7ffb0c3c674b
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ module copyrightcrusader.org/inv
|
|||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require github.com/lib/pq v1.10.2 // indirect
|
require github.com/lib/pq v1.10.2
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Go Best practices: https://github.com/golovers/effective-go
|
//Go Best practices: https://github.com/golovers/effective-go
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Id int32 `json:"id"`
|
ID int32 `json:"id"`
|
||||||
Name int32 `json:"name"`
|
Name int32 `json:"name"`
|
||||||
ValueCents int32 `json:"valueCents"`
|
ValueCents int32 `json:"valueCents"`
|
||||||
PurchaseCostCents int32 `json:"purchasedCostCents"`
|
PurchaseCostCents int32 `json:"purchasedCostCents"`
|
||||||
@@ -27,8 +27,11 @@ type Item struct {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
http.HandleFunc("/categories", categories)
|
http.HandleFunc("/categories", categories)
|
||||||
http.HandleFunc("/category", category)
|
//Note: seems adding a trailing slash allows routing URLs like /category/ID#
|
||||||
err := http.ListenAndServe(":8080", nil)
|
//No need for query parameters.
|
||||||
|
http.HandleFunc("/category/", category)
|
||||||
|
|
||||||
|
err := http.ListenAndServe(":8088", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -39,7 +42,7 @@ func main() {
|
|||||||
func categories(w http.ResponseWriter, r *http.Request) {
|
func categories(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "GET":
|
case "GET":
|
||||||
getCategories(w, r)
|
getCategoriesHandler(w, r)
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
w.Write([]byte("Method not allowed"))
|
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) {
|
func category(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "GET":
|
case "GET":
|
||||||
getCategory(w, r)
|
getCategoryHandler(w, r)
|
||||||
case "POST":
|
case "POST":
|
||||||
insertCategory(w, r)
|
insertCategoryHandler(w, r)
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
deleteCategory(w, r)
|
deleteCategoryHandler(w, r)
|
||||||
default:
|
default:
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
w.Write([]byte("Method not allowed"))
|
w.Write([]byte("Method not allowed"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCategory(w http.ResponseWriter, r *http.Request) {
|
func getCategoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get("id")
|
id := strings.TrimPrefix(r.URL.Path, "/category/")
|
||||||
|
|
||||||
value, err := strconv.ParseInt(id, 0, 64)
|
value, err := strconv.ParseInt(id, 0, 64)
|
||||||
|
|
||||||
@@ -71,19 +74,11 @@ func getCategory(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db := getSQLConnection()
|
category, ok := getCategoryByID(value)
|
||||||
|
|
||||||
defer db.Close()
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
var category Category
|
w.Write([]byte("Failed to get 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()))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +94,8 @@ func getCategory(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(jsonBytes)
|
w.Write(jsonBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteCategory(w http.ResponseWriter, r *http.Request) {
|
func deleteCategoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get("id")
|
id := strings.TrimPrefix(r.URL.Path, "/category/")
|
||||||
|
|
||||||
value, err := strconv.ParseInt(id, 0, 64)
|
value, err := strconv.ParseInt(id, 0, 64)
|
||||||
|
|
||||||
@@ -110,24 +105,23 @@ func deleteCategory(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db := getSQLConnection()
|
if !deleteCategory(value) {
|
||||||
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
_, err = db.Exec("DELETE FROM category WHERE category_id = $1;", value)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(fmt.Sprintf("Failed to delete %d", value)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(fmt.Sprintf("Deleted category '%d'.", value)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCategories(w http.ResponseWriter, r *http.Request) {
|
func getCategoriesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
categories := getAllCategories()
|
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)
|
jsonBytes, err := json.Marshal(categories)
|
||||||
|
|
||||||
@@ -141,12 +135,12 @@ func getCategories(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(jsonBytes)
|
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")
|
ct := r.Header.Get("content-type")
|
||||||
|
|
||||||
if ct != "application/json" {
|
if ct != "application/json" {
|
||||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,17 +164,11 @@ func insertCategory(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db := getSQLConnection()
|
id, ok := insertNewCategory(category.Name)
|
||||||
|
|
||||||
defer db.Close()
|
if !ok {
|
||||||
|
|
||||||
id := 0
|
|
||||||
|
|
||||||
err = db.QueryRow("INSERT INTO category (category_name) VALUES ($1) RETURNING category_id;", category.Name).Scan(&id)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte("Failed to create the new category."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user