api.go (304 lines of code) (raw):
package main
import (
"encoding/csv"
"io"
"net/http"
"strconv"
"time"
"github.com/gin-contrib/cache"
"github.com/gin-contrib/cache/persistence"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"go.elastic.co/apm/v2"
)
func addAPIHandlers(r *gin.RouterGroup, db *sqlx.DB) {
h := apiHandlers{db}
r.GET("/stats", h.getStats)
r.GET("/products", h.getProducts)
r.GET("/products/:id", h.getProductDetails)
r.GET("/products/:id/customers", h.getProductCustomers)
r.GET("/types", h.getProductTypes)
r.GET("/types/:id", h.getProductTypeDetails)
r.GET("/customers", h.getCustomers)
r.GET("/customers/:id", h.getCustomerDetails)
r.GET("/orders", h.getOrders)
r.GET("/orders/:id", h.getOrderDetails)
r.POST("/orders", h.postOrder)
r.POST("/orders/csv", h.postOrderCSV)
}
type apiHandlers struct {
db *sqlx.DB
}
func (h apiHandlers) getStats(c *gin.Context) {
cacheValue, _ := c.Get(cache.CACHE_MIDDLEWARE_KEY)
cache := *cacheValue.(*persistence.CacheStore)
const cacheKey = "shop-stats"
var stats *Stats
err := cache.Get(cacheKey, &stats)
switch err {
case nil:
contextLogger(c).Debug("serving stats from cache")
c.JSON(http.StatusOK, stats)
if tx := apm.TransactionFromContext(c.Request.Context()); tx != nil {
tx.Context.SetLabel("served_from_cache", "true")
}
return
case persistence.ErrCacheMiss:
// fetch and cache below
if tx := apm.TransactionFromContext(c.Request.Context()); tx != nil {
tx.Context.SetLabel("served_from_cache", "false")
}
break
default:
err := errors.Wrap(err, "failed to get stats from cache")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
stats, err = getStats(c.Request.Context(), h.db)
if err != nil {
err := errors.Wrap(err, "failed to query stats")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if err := cache.Set(cacheKey, stats, time.Minute); err != nil {
err := errors.Wrap(err, "failed to cache stats")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
contextLogger(c).Debug("cached stats")
c.JSON(http.StatusOK, stats)
}
func (h apiHandlers) getProducts(c *gin.Context) {
products, err := getProducts(c.Request.Context(), h.db)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, products)
}
func (h apiHandlers) getTopProducts(c *gin.Context) {
products, err := getTopProducts(c.Request.Context(), h.db)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, products)
}
func (h apiHandlers) getProductDetails(c *gin.Context) {
idString := c.Param("id")
if idString == "top" {
products, err := getTopProducts(c.Request.Context(), h.db)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, products)
return
}
// Product by ID.
id, err := strconv.Atoi(idString)
if err != nil {
err := errors.Wrap(err, "failed to parse product ID")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
product, err := getProduct(c.Request.Context(), h.db, id)
if err != nil {
err := errors.Wrap(err, "failed to get product")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if product == nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.JSON(http.StatusOK, product)
}
func (h apiHandlers) getProductCustomers(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
limit := 1000
if countString := c.Param("count"); countString != "" {
limit, err = strconv.Atoi(countString)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
customers, err := getProductCustomers(c.Request.Context(), h.db, id, limit)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, customers)
}
func (h apiHandlers) getProductTypes(c *gin.Context) {
productTypes, err := getProductTypes(c.Request.Context(), h.db)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, productTypes)
}
func (h apiHandlers) getProductTypeDetails(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
err := errors.Wrap(err, "failed to parse product type ID")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
productType, err := getProductType(c.Request.Context(), h.db, id)
if err != nil {
err := errors.Wrap(err, "failed to get product type details")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if productType == nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.JSON(http.StatusOK, productType)
}
func (h apiHandlers) getCustomers(c *gin.Context) {
customers, err := getCustomers(c.Request.Context(), h.db)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, customers)
}
func (h apiHandlers) getCustomerDetails(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
err := errors.Wrap(err, "failed to parse customer ID")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
customer, err := getCustomer(c.Request.Context(), h.db, id)
if err != nil {
err := errors.Wrap(err, "failed to get customer details")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if customer == nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.JSON(http.StatusOK, customer)
}
func (h apiHandlers) getOrders(c *gin.Context) {
orders, err := getOrders(c.Request.Context(), h.db)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, orders)
}
func (h apiHandlers) getOrderDetails(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
customer, err := getOrder(c.Request.Context(), h.db, id)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if customer == nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.JSON(http.StatusOK, customer)
}
func (h apiHandlers) postOrder(c *gin.Context) {
type line struct {
ID int `json:"id" binding:"required"`
Amount int `json:"amount" binding:"required"`
}
var order struct {
CustomerID int `json:"customer_id" binding:"required"`
Lines []line `json:"lines" binding:"required"`
}
if err := c.BindJSON(&order); err != nil {
return
}
lines := make([]ProductOrderLine, len(order.Lines))
for i, line := range order.Lines {
lines[i] = ProductOrderLine{
Product: Product{ID: line.ID},
Amount: line.Amount,
}
}
h.postOrderCommon(c, order.CustomerID, lines)
}
func (h apiHandlers) postOrderCSV(c *gin.Context) {
customerID, err := strconv.Atoi(c.PostForm("customer"))
if err != nil {
err := errors.Wrap(err, "failed to parse customer ID")
c.AbortWithError(http.StatusBadRequest, err)
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
err := errors.Wrap(err, "failed get CSV file")
c.AbortWithError(http.StatusBadRequest, err)
return
}
file, err := fileHeader.Open()
if err != nil {
err := errors.Wrap(err, "failed open CSV file")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
defer file.Close()
var lines []ProductOrderLine
r := csv.NewReader(file)
for {
record, err := r.Read()
if err == io.EOF {
break
} else if err != nil {
err := errors.Wrap(err, "failed to parse CSV file")
c.AbortWithError(http.StatusBadRequest, err)
return
}
productID, err := strconv.Atoi(record[0])
if err != nil {
err := errors.Wrap(err, "failed to parse product ID")
c.AbortWithError(http.StatusBadRequest, err)
return
}
amount, err := strconv.Atoi(record[1])
if err != nil {
err := errors.Wrap(err, "failed to parse order amount")
c.AbortWithError(http.StatusBadRequest, err)
return
}
lines = append(lines, ProductOrderLine{
Product: Product{ID: productID},
Amount: amount,
})
}
h.postOrderCommon(c, customerID, lines)
}
func (h apiHandlers) postOrderCommon(c *gin.Context, customerID int, lines []ProductOrderLine) {
customer, err := getCustomer(c.Request.Context(), h.db, customerID)
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
orderID, err := createOrder(c.Request.Context(), h.db, customer, lines)
if err != nil {
err := errors.Wrap(err, "failed to create order")
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if tx := apm.TransactionFromContext(c.Request.Context()); tx != nil {
tx.Context.SetLabel("customer_name", customer.FullName)
tx.Context.SetLabel("customer_email", customer.Email)
}
c.JSON(http.StatusOK, gin.H{"id": orderID})
}