Add user posts
Tags: go, gin, postgres
Published: 2021-06-24 17:51:15

With authentication in place, it is time to start using it. We will need authentication to create, read, update and delete user's blog posts. Let's start by adding new database migration which will create required data table with columns. Create new migration file `migrations/2_addPostsTable.go`:

```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
  })
}
```

And then run migrations with:

```bash
cd migrations/
go run *.go up
```

Now we create structure to hold post data. We will also add field constraints for Title and Content. Add new file `internal/store/posts.go`:

```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:"-"`
}
```

User can have multiple blog posts, so we have to add `has-many` relation to User struct. In `internal/store/users.go`, edit User struct:

```go
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"`
}
```

Function that can insert new post entry in database will be implemented in `internal/store/posts.go`:

```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
}
```

To create post, we will add new handler which will call the function above. Create new file `internal/server/post.go`:

```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,
  })
}
```

Post creation handler is ready, so let's add new protected route for crating posts. In `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:

```go
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
}
```

The recipe is same for all other CRUD (Create, Read, Update, Delete) methods:

1. implement function to communicate with database for required action
2. implement Gin handler which will use function from step 1
3. add route with handler to router

We have `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`:

```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
}
```

And in `internal/server/post.go`:

```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,
  })
}
```

To `Update` post, add these 2 functions to `internal/store/posts.go`:

```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
}
```

And in `internal/server/post.go`:

```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,
  })
}
```

Finally, to `Delete` post add to `internal/store.posts.go`:

```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
}
```

And in `internal/server/post.go`:

```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."})
}
```

One new thing that you can notice here is `paramID := ctx.Param("id")`. We are using that to extract ID param from URL path.

Let's add all those handlers to router:

```go
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
}
```

If user has no posts yet, `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`:

```go
var _ pg.AfterSelectHook = (*User)(nil)

func (user *User) AfterSelect(ctx context.Context) error {
  if user.Posts == nil {
    user.Posts = []*Post{}
  }
  return nil
}
```