This commit is contained in:
Tyrel Souza 2022-10-17 11:51:16 -04:00
parent 05d9046e15
commit b5f71890a6
No known key found for this signature in database
GPG Key ID: F6582CF1308A2360
14 changed files with 483 additions and 171 deletions

View File

@ -1,84 +0,0 @@
package controllers
import (
"net/http"
"strconv"
"web-service-gin/models"
"github.com/gin-gonic/gin"
)
func FindAlbums(c *gin.Context) {
var albums []models.Album
models.DB.Find(&albums)
c.JSON(http.StatusOK, gin.H{"data": albums})
}
func FindAlbum(c *gin.Context) {
var album models.Album
if err := models.DB.Where("id = ?", c.Param("id")).First(&album).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
}
c.JSON(http.StatusOK, gin.H{"data": album})
}
func CreateAlbum(c *gin.Context) {
var input models.CreateAlbumInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var price float64
if f, err := strconv.ParseFloat(input.Price, 64); err == nil {
price = f
}
album := models.Album{
Title: input.Title,
Artist: input.Artist,
Price: price,
}
models.DB.Create(&album)
c.JSON(http.StatusOK, gin.H{"data": album})
}
func UpdateAlbum(c *gin.Context) {
var album models.Album
if err := models.DB.Where("id = ?", c.Param("id")).First(&album).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
}
var input models.UpdateAlbumInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
models.DB.Model(&album).Updates(input)
c.JSON(http.StatusOK, gin.H{"data": album})
}
func DeleteAlbum(c *gin.Context) {
var album models.Album
if err := models.DB.Where("id = ?", c.Param("id")).First(&album).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
models.DB.Delete(&album)
c.JSON(http.StatusOK, gin.H{"data": true})
}
func ApplyAlbumRouter(router *gin.Engine) *gin.Engine {
router.GET("/albums", FindAlbums)
router.POST("/albums", CreateAlbum)
router.GET("/albums/:id", FindAlbum)
router.PATCH("/albums/:id", UpdateAlbum)
router.DELETE("/albums/:id", DeleteAlbum)
return router
}

8
go.mod
View File

@ -4,8 +4,9 @@ go 1.19
require (
github.com/gin-gonic/gin v1.8.1
gorm.io/driver/mysql v1.4.1
gorm.io/gorm v1.24.0
github.com/go-sql-driver/mysql v1.6.0
github.com/guregu/null v4.0.0+incompatible
github.com/jmoiron/sqlx v1.3.5
)
require (
@ -13,10 +14,7 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect

18
go.sum
View File

@ -22,11 +22,10 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw=
github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -39,8 +38,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
@ -91,8 +94,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.1 h1:4InA6SOaYtt4yYpV1NF9B2kvUKe9TbvUd1iWrvxnjic=
gorm.io/driver/mysql v1.4.1/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=

View File

@ -0,0 +1,177 @@
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"net/http"
"web-service-gin/internal/forms"
models2 "web-service-gin/internal/models"
)
// Album provides the handlers for the album entity.
type Album struct {
albumService models2.AlbumService
}
// NewAlbum creates the controller using the given data mapper for
// albums.
func NewAlbum(albumService models2.AlbumService) *Album {
return &Album{
albumService: albumService,
}
}
// Post will create a new album from the given data, if the form is valid.
func (p *Album) Post(c *gin.Context) {
var form forms.CreateAlbum
if c.ShouldBindWith(&form, binding.JSON) != nil {
// TODO: Give a better error message.
c.JSON(
http.StatusNotAcceptable,
gin.H{"message": "invalid data."},
)
c.Abort()
return
}
album, err := p.albumService.Create(&form)
if err != nil {
// TODO: An error middleware should log the error,
// and email admin.
c.Error(err)
c.JSON(
http.StatusInternalServerError,
gin.H{"message": "internal error"},
)
c.Abort()
return
}
// TODO: use a view if part of the album data should not be
// returned to the client.
c.JSON(
http.StatusCreated,
album,
)
}
// Put will perform an update of a album.
func (p *Album) Put(c *gin.Context) {
var form forms.CreateAlbum
if err := c.ShouldBindWith(&form, binding.JSON); err != nil {
// TODO: Give a better error message.
c.JSON(
http.StatusNotAcceptable,
gin.H{
"message": "invalid data.",
"form": form,
"error": err.Error(),
},
)
c.Abort()
return
}
id := c.Param("id")
album, err := p.albumService.GetByID(id)
if err == models2.ErrNotFound {
c.JSON(
http.StatusNotFound,
gin.H{"message": "user not found"},
)
c.Abort()
return
} else if err != nil {
c.Error(err)
c.JSON(
http.StatusInternalServerError,
gin.H{"message": "internal error."},
)
c.Abort()
return
}
album.ApplyForm(&form)
err = p.albumService.Update(album)
if err != nil {
c.Error(err)
c.JSON(
http.StatusInternalServerError,
gin.H{"message": "internal error."},
)
c.Abort()
return
}
c.JSON(
http.StatusOK,
gin.H{"message": "updated"},
)
}
// Get will fetch an album by ID.
func (p *Album) Get(c *gin.Context) {
id := c.Param("id")
album, err := p.albumService.GetByID(id)
if err == models2.ErrNotFound {
c.JSON(
http.StatusNotFound,
gin.H{"message": "user not found"},
)
c.Abort()
return
} else if err != nil {
c.Error(err)
c.JSON(
http.StatusInternalServerError,
gin.H{"message": "internal error."},
)
c.Abort()
return
}
c.JSON(
http.StatusOK,
album,
)
}
// GetAll will fetch all Albums.
// TODO: Pagination
func (p *Album) GetAll(c *gin.Context) {
albums, err := p.albumService.GetAll()
if err != nil {
c.Error(err)
c.JSON(
http.StatusInternalServerError,
gin.H{"message": "internal error."},
)
c.Abort()
return
}
c.JSON(
http.StatusOK,
albums,
)
}
// Delete will remove a album from the DB.
func (p *Album) Delete(c *gin.Context) {
id := c.Param("id")
err := p.albumService.Delete(id)
if err != nil {
c.Error(err)
c.JSON(
http.StatusInternalServerError,
gin.H{"message": "internal error."},
)
c.Abort()
}
c.JSON(
http.StatusOK,
gin.H{"message": "deleted"},
)
}

12
internal/forms/album.go Normal file
View File

@ -0,0 +1,12 @@
package forms
type CreateAlbum struct {
ID *int64 `form:"id" json:"id" binding:"required"`
Title *string `form:"title" json:"title" binding:"required"`
Artist *string `form:"artist" json:"artist" binding:"required"`
Price *float64 `form:"price" json:"price" binding:"required"`
CreatedAt *string `form:"created_at" json:"created_at"`
UpdatedAt *string `form:"updated_at" json:"updated_at"`
DeletedAt *string `form:"deleted_at" json:"deleted_at"`
}

34
internal/models/album.go Normal file
View File

@ -0,0 +1,34 @@
package models
import (
"github.com/guregu/null"
"web-service-gin/internal/forms"
)
type Album struct {
ID int64 `db:"id"`
CreatedAt null.String `db:"created_at"`
UpdatedAt null.String `db:"updated_at"`
DeletedAt null.String `db:"deleted_at"`
Title string `db:"title"`
Artist string `db:"artist"`
Price float64 `db:"price"`
}
func (a *Album) ApplyForm(form *forms.CreateAlbum) {
a.ID = *form.ID
a.Title = *form.Title
a.Artist = *form.Artist
a.Price = *form.Price
a.CreatedAt = null.StringFromPtr(form.CreatedAt)
a.UpdatedAt = null.StringFromPtr(form.UpdatedAt)
a.DeletedAt = null.StringFromPtr(form.DeletedAt)
}
type AlbumService interface {
Create(*forms.CreateAlbum) (*Album, error)
GetByID(string) (*Album, error)
GetAll() (*[]Album, error)
Update(*Album) error
Delete(string) error
}

View File

@ -0,0 +1,5 @@
package models
import "errors"
var ErrNotFound = errors.New("object not found")

View File

@ -0,0 +1,148 @@
package sql
import (
"database/sql"
"github.com/jmoiron/sqlx"
"web-service-gin/internal/forms"
models2 "web-service-gin/internal/models"
)
// AlbumService is the implementation of the album data mapping layer
// using SQL.
type AlbumService struct {
conn *sqlx.DB
}
// NewAlbumService creates the album service using the given
// connection pool to a mysql DB.
func NewAlbumService(conn *sqlx.DB) (*AlbumService, error) {
// TODO: It would be better to use a DB management tool
// to make migrations painless.
// _, err := conn.Exec(`
//CREATE TABLE albums (
// id bigint NOT NULL AUTO_INCREMENT,
// title longtext,
// artist longtext,
// price double DEFAULT NULL,
// PRIMARY KEY (id),
// KEY idx_albums_deleted_at (deleted_at)
//) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
//`)
// if err != nil {
// return nil, err
// }
return &AlbumService{conn: conn}, nil
}
// Create will try to add the album to the DB.
func (s *AlbumService) Create(form *forms.CreateAlbum) (*models2.Album, error) {
q := `
INSERT INTO albums(title, artist, price)
VALUES (?, ?, ?)
RETURNING *;`
var output models2.Album
err := s.conn.Get(
&output,
q,
*form.Title,
*form.Artist,
*form.Price,
)
if err != nil {
return nil, err
}
return &output, nil
}
// Update will replace the values of the give album with those provided.
func (s *AlbumService) Update(p *models2.Album) error {
q := `
UPDATE albums
SET updated_at = NOW(),
title = ?,
artist = ?,
price = ?
WHERE id = ?;
`
_, err := s.conn.Exec(
q,
p.Title,
p.Artist,
p.Price,
p.ID,
)
if err != nil {
return err
}
return nil
}
// GetByID fetches the album with the given id.
func (s *AlbumService) GetByID(id string) (*models2.Album, error) {
if !validID(id) {
return nil, models2.ErrNotFound
}
q := `
SELECT *
FROM albums
WHERE id = ?;`
var output models2.Album
err := s.conn.Get(
&output,
q,
id,
)
// Replace the SQL error with our own error type.
if err == sql.ErrNoRows {
return nil, models2.ErrNotFound
} else if err != nil {
return nil, err
} else {
return &output, nil
}
}
// GetAll fetches all albums.
func (s *AlbumService) GetAll() (*[]models2.Album, error) {
q := `SELECT * FROM albums;`
var output []models2.Album
err := s.conn.Select(&output, q)
// Replace the SQL error with our own error type.
if err == sql.ErrNoRows {
return nil, models2.ErrNotFound
} else if err != nil {
return nil, err
} else {
return &output, nil
}
}
// Delete removes the album with the given id from the DB.
// TODO: this should just mark the object as deleted,
// not actually get rid of the data.
func (s *AlbumService) Delete(id string) error {
if !validID(id) {
return models2.ErrNotFound
}
q := `
DELETE FROM albums
WHERE id = ?;
`
_, err := s.conn.Exec(
q,
id,
)
return err
}
// Check it implements the interface
var _ models2.AlbumService = &AlbumService{}

View File

@ -0,0 +1,36 @@
package sql
import (
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"strconv"
)
// NewSQL creates and SQL connection using environment variables
// to configure.
func NewSQL() (*sqlx.DB, error) {
//host := strings.TrimSpace(os.Getenv("MYSQL_HOST"))
//port := strings.TrimSpace(os.Getenv("MYSQL_PORT"))
//user := strings.TrimSpace(os.Getenv("MYSQL_USER"))
//password := strings.TrimSpace(os.Getenv("MYSQL_PASSWORD"))
//db := strings.TrimSpace(os.Getenv("MYSQL_DB"))
//
//info := fmt.Sprintf(
// "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
// host,
// port,
// user,
// password,
// db,
//)
return sqlx.Connect(
"mysql",
"mysql:password@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=True&loc=Local",
)
}
// validID checks if the given string is a valid id.
func validID(id string) bool {
_, err := strconv.Atoi(id)
return err == nil
}

45
internal/server/server.go Normal file
View File

@ -0,0 +1,45 @@
package server
import (
"github.com/gin-gonic/gin"
"web-service-gin/internal/controllers"
"web-service-gin/internal/models"
"web-service-gin/internal/models/sql"
)
// Server represents all the services and controllers.
type Server struct {
AlbumService models.AlbumService
Gin *gin.Engine
}
// NewServer creates a new server using environment variables to
// configure DB connection.
func NewServer() (*Server, error) {
db, err := sql.NewSQL()
if err != nil {
return nil, err
}
albumService, err := sql.NewAlbumService(db)
if err != nil {
return nil, err
}
r := gin.Default()
{
route := r.Group("/albums")
ctrl := controllers.NewAlbum(albumService)
route.GET("", ctrl.GetAll)
route.POST("", ctrl.Post)
route.PUT("/:id", ctrl.Put)
route.GET("/:id", ctrl.Get)
route.DELETE("/:id", ctrl.Delete)
}
return &Server{
AlbumService: albumService,
Gin: r,
}, nil
}

35
main.go
View File

@ -1,34 +1,17 @@
package main
import "web-service-gin/internal/server"
// Initial based on:
// https://go.dev/doc/tutorial/web-service-gin
// https://blog.logrocket.com/how-to-build-a-rest-api-with-golang-using-gin-and-gorm/
import (
"net/http"
"web-service-gin/controllers"
"web-service-gin/models"
"github.com/gin-gonic/gin"
)
func SetupRouter() *gin.Engine {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
router = controllers.ApplyAlbumRouter(router)
return router
}
// Then moved to Sqlx
// https://github.com/wetterj/gin-sqlx-crud
func main() {
router := SetupRouter()
models.ConnectDatabase()
s := &http.Server{
Addr: ":8123",
Handler: router,
server, err := server.NewServer()
if err != nil {
panic(err)
}
s.ListenAndServe()
server.Gin.Run()
}

View File

@ -1,20 +0,0 @@
package models
type Album struct {
ID string `json:"id" gorm:"primary_key"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
type CreateAlbumInput struct {
Title string `json:"title" binding:"required"`
Artist string `json:"artist" binding:"required"`
Price string `json:"price" binding:"required"`
}
type UpdateAlbumInput struct {
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}

View File

@ -1,20 +0,0 @@
package models
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
// Obviously change this to your settings and get securely.
dsn := "mysql:password@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("Failed to connect to database")
}
_ = db.AutoMigrate(&Album{})
DB = db
}

View File

@ -1,31 +1,31 @@
GET http://localhost:8123/albums/4
GET http://localhost:8080/albums/4
Content-Type: application/json
###
GET http://localhost:8123/albums
GET http://localhost:8080/albums
Content-Type: application/json
###
POST http://localhost:8123/albums
POST http://localhost:8080/albums
Content-Type: application/json
{"title": "Mark Tom and Travis Show", "artist": "Blink-182", "price": "18.99"}
###
POST http://localhost:8123/albums
POST http://localhost:8080/albums
Content-Type: application/json
{"title": "Blue Train", "artist": "John Coltrane", "price": 56.99}
###
POST http://localhost:8123/albums
POST http://localhost:8080/albums
Content-Type: application/json
{"title": "Jeru", "artist": "Gerry Mulligan", "price": 56.99}
###
POST http://localhost:8123/albums
POST http://localhost:8080/albums
Content-Type: application/json
{"title": "Sarah Vaughan and Clifford Brown", "artist": "Sarah Vaughan", "price": 39.99}