29
loading...
This website collects cookies to deliver better user experience
migrations/2_addPostsTable.go
:package main
import (
"fmt"
"github.com/go-pg/migrations/v8"
)
func init() {
migrations.MustRegisterTx(func(db migrations.DB) error {
fmt.Println("creating table posts...")
_, err := db.Exec(`CREATE TABLE posts(
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
user_id INT REFERENCES users ON DELETE CASCADE
)`)
return err
}, func(db migrations.DB) error {
fmt.Println("dropping table posts...")
_, err := db.Exec(`DROP TABLE posts`)
return err
})
}
cd migrations/
go run *.go up
internal/store/posts.go
:package store
import "time"
type Post struct {
ID int
Title string `binding:"required,min=3,max=50"`
Content string `binding:"required,min=5,max=5000"`
CreatedAt time.Time
ModifiedAt time.Time
UserID int `json:"-"`
}
has-many
relation to User struct. In internal/store/users.go
, edit User struct:type User struct {
ID int
Username string `binding:"required,min=5,max=30"`
Password string `pg:"-" binding:"required,min=7,max=32"`
HashedPassword []byte `json:"-"`
Salt []byte `json:"-"`
CreatedAt time.Time
ModifiedAt time.Time
Posts []*Post `json:"-" pg:"fk:user_id,rel:has-many,on_delete:CASCADE"`
}
internal/store/posts.go
:func AddPost(user *User, post *Post) error {
post.UserID = user.ID
_, err := db.Model(post).Returning("*").Insert()
if err != nil {
log.Error().Err(err).Msg("Error inserting new post")
}
return err
}
internal/server/post.go
:package server
import (
"net/http"
"rgb/internal/store"
"github.com/gin-gonic/gin"
)
func createPost(ctx *gin.Context) {
post := new(store.Post)
if err := ctx.Bind(post); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := currentUser(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := store.AddPost(user, post); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "Post created successfully.",
"data": post,
})
}
internal/server/router.go
, we will create new group which will use authorization
middleware we implemented in previous chapter. We will add route /posts
with HTTP method POST to that protected group:func setRouter() *gin.Engine {
// Creates default gin router with Logger and Recovery middleware already attached
router := gin.Default()
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
router.RedirectTrailingSlash = true
// Create API route group
api := router.Group("/api")
{
api.POST("/signup", signUp)
api.POST("/signin", signIn)
}
authorized := api.Group("/")
authorized.Use(authorization)
{
authorized.POST("/posts", createPost)
}
router.NoRoute(func(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{}) })
return router
}
Create
part covered, so let's move on to the next method, Read
. We will implement function that will fetch all user's posts from database in internal/store/posts.go
:func FetchUserPosts(user *User) error {
err := db.Model(user).
Relation("Posts", func(q *orm.Query) (*orm.Query, error) {
return q.Order("id ASC"), nil
}).
Select()
if err != nil {
log.Error().Err(err).Msg("Error fetching user's posts")
}
return err
}
internal/server/post.go
:func indexPosts(ctx *gin.Context) {
user, err := currentUser(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := store.FetchUserPosts(user); err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "Posts fetched successfully.",
"data": user.Posts,
})
}
Update
post, add these 2 functions to internal/store/posts.go
:func FetchPost(id int) (*Post, error) {
post := new(Post)
post.ID = id
err := db.Model(post).WherePK().Select()
if err != nil {
log.Error().Err(err).Msg("Error fetching post")
return nil, err
}
return post, nil
}
func UpdatePost(post *Post) error {
_, err := db.Model(post).WherePK().UpdateNotZero()
if err != nil {
log.Error().Err(err).Msg("Error updating post")
}
return err
}
internal/server/post.go
:func updatePost(ctx *gin.Context) {
jsonPost := new(store.Post)
if err := ctx.Bind(jsonPost); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := currentUser(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
dbPost, err := store.FetchPost(jsonPost.ID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if user.ID != dbPost.UserID {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Not authorized."})
return
}
jsonPost.ModifiedAt = time.Now()
if err := store.UpdatePost(jsonPost); err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"msg": "Post updated successfully.",
"data": jsonPost,
})
}
Delete
post add to internal/store.posts.go
:func DeletePost(post *Post) error {
_, err := db.Model(post).WherePK().Delete()
if err != nil {
log.Error().Err(err).Msg("Error deleting post")
}
return err
}
internal/server/post.go
:func deletePost(ctx *gin.Context) {
paramID := ctx.Param("id")
id, err := strconv.Atoi(paramID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Not valid ID."})
return
}
user, err := currentUser(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
post, err := store.FetchPost(id)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if user.ID != post.UserID {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Not authorized."})
return
}
if err := store.DeletePost(post); err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"msg": "Post deleted successfully."})
}
paramID := ctx.Param("id")
. We are using that to extract ID param from URL path.func setRouter() *gin.Engine {
// Creates default gin router with Logger and Recovery middleware already attached
router := gin.Default()
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
router.RedirectTrailingSlash = true
// Create API route group
api := router.Group("/api")
{
api.POST("/signup", signUp)
api.POST("/signin", signIn)
}
authorized := api.Group("/")
authorized.Use(authorization)
{
authorized.GET("/posts", indexPosts)
authorized.POST("/posts", createPost)
authorized.PUT("/posts", updatePost)
authorized.DELETE("/posts/:id", deletePost)
}
router.NoRoute(func(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{}) })
return router
}
User.Posts
field will be nil by default. This complicates things for frontend since it must check for nil value, so it would be better to use empty slice. For that we will use AfterSelectHook
which will be executed every time after Select()
is executed for User
. That hook will be added to internal/store/users.go
:var _ pg.AfterSelectHook = (*User)(nil)
func (user *User) AfterSelect(ctx context.Context) error {
if user.Posts == nil {
user.Posts = []*Post{}
}
return nil
}