Skip to content

Commit

Permalink
[BE] Follow APIs (#30)
Browse files Browse the repository at this point in the history
Added followers/following APIs

---------

Signed-off-by: Kshitij Patil <kshitijpatil98@gmail.com>
  • Loading branch information
Kshitij09 authored Aug 6, 2024
1 parent 23abb28 commit f57540c
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 0 deletions.
62 changes: 62 additions & 0 deletions backend/domain/follow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package domain

import (
"fmt"
"github.com/Kshitij09/snakechat_server/domain/paging"
)

const FollowPageSize = 20

type FollowUser struct {
Id string
Name string
ProfileUrl *string
UpdatedAt int64
}

func (f FollowUser) OffsetKey() int64 {
return f.UpdatedAt
}

type FollowDao interface {
Followers(userId string) ([]FollowUser, error)
FollowersUpdatedBefore(userId string, updateTimestamp int64) ([]FollowUser, error)
Following(userId string) ([]FollowUser, error)
FollowingUpdatedBefore(userId string, updateTimestamp int64) ([]FollowUser, error)
}

type FollowService struct {
followers FollowDao
}

func NewFollowService(followers FollowDao) *FollowService {
return &FollowService{followers: followers}
}

func (s *FollowService) Followers(userId string, offset *string) (*paging.Page[int64, FollowUser], error) {
fetcher := paging.Fetcher[int64, FollowUser]{
ById: s.followers.Followers,
ByIdAndOffset: s.followers.FollowersUpdatedBefore,
OffsetConv: timestampConverter[FollowUser]{},
PageSize: FollowPageSize,
}
followers, err := fetcher.FetchPage(userId, offset)
if err != nil {
return nil, fmt.Errorf("followers: %w", err)
}
return followers, nil
}

func (s *FollowService) Following(userId string, offset *string) (*paging.Page[int64, FollowUser], error) {
fetcher := paging.Fetcher[int64, FollowUser]{
ById: s.followers.Following,
ByIdAndOffset: s.followers.FollowingUpdatedBefore,
OffsetConv: timestampConverter[FollowUser]{},
PageSize: FollowPageSize,
}
followers, err := fetcher.FetchPage(userId, offset)
if err != nil {
return nil, fmt.Errorf("following: %w", err)
}
return followers, nil
}
104 changes: 104 additions & 0 deletions backend/sqlite/follow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package sqlite

import (
"database/sql"
"fmt"
"github.com/Kshitij09/snakechat_server/domain"
)

type FollowsStorage struct {
db *sql.DB
}

func NewFollowsStorage(db *sql.DB) *FollowsStorage {
return &FollowsStorage{db}
}

func (ctx FollowsStorage) Followers(userId string) ([]domain.FollowUser, error) {
query := `
SELECT u.id, u.name, u.profile_url, f.updated_at
FROM followers f INNER JOIN users u ON f.follower_id = u.id
WHERE user_id = ?
ORDER BY updated_at DESC
LIMIT ?
`
likers, err := ctx.queryFollows(query, userId, domain.FollowPageSize)
if err != nil {
return nil, fmt.Errorf("FollowersStorage: %w", err)
}
return likers, nil
}

func (ctx FollowsStorage) FollowersUpdatedBefore(userId string, updateTimestamp int64) ([]domain.FollowUser, error) {
query := `
SELECT u.id, u.name, u.profile_url, f.updated_at
FROM followers f INNER JOIN users u ON f.follower_id = u.id
WHERE user_id = ?
AND updated_at < ?
ORDER BY updated_at DESC
LIMIT ?
`
likers, err := ctx.queryFollows(query, userId, updateTimestamp, domain.FollowPageSize)
if err != nil {
return nil, fmt.Errorf("FollowersStorage: %w", err)
}
return likers, nil
}

func (ctx FollowsStorage) Following(userId string) ([]domain.FollowUser, error) {
query := `
SELECT u.id, u.name, u.profile_url, f.updated_at
FROM followers f INNER JOIN users u ON f.user_id = u.id
WHERE follower_id = ?
ORDER BY updated_at DESC
LIMIT ?
`
likers, err := ctx.queryFollows(query, userId, domain.FollowPageSize)
if err != nil {
return nil, fmt.Errorf("FollowersStorage: %w", err)
}
return likers, nil
}

func (ctx FollowsStorage) FollowingUpdatedBefore(userId string, updateTimestamp int64) ([]domain.FollowUser, error) {
query := `
SELECT u.id, u.name, u.profile_url, f.updated_at
FROM followers f INNER JOIN users u ON f.user_id = u.id
WHERE follower_id = ?
AND updated_at < ?
ORDER BY updated_at DESC
LIMIT ?
`
likers, err := ctx.queryFollows(query, userId, updateTimestamp, domain.FollowPageSize)
if err != nil {
return nil, fmt.Errorf("FollowersStorage: %w", err)
}
return likers, nil
}

func (ctx FollowsStorage) queryFollows(query string, args ...any) ([]domain.FollowUser, error) {
rs, err := ctx.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rs.Close()
return scanFollowUser(rs)
}

func scanFollowUser(rows *sql.Rows) ([]domain.FollowUser, error) {
follows := make([]domain.FollowUser, 0)
for rows.Next() {
var user domain.FollowUser
err := rows.Scan(
&user.Id,
&user.Name,
&user.ProfileUrl,
&user.UpdatedAt,
)
if err != nil {
return nil, err
}
follows = append(follows, user)
}
return follows, nil
}
96 changes: 96 additions & 0 deletions backend/transport/follow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package transport

import (
"database/sql"
"encoding/json"
"errors"
"github.com/Kshitij09/snakechat_server/domain"
"github.com/Kshitij09/snakechat_server/domain/offsetconv"
"github.com/Kshitij09/snakechat_server/domain/paging"
"github.com/Kshitij09/snakechat_server/sqlite"
"github.com/Kshitij09/snakechat_server/transport/apierror"
"github.com/Kshitij09/snakechat_server/transport/handlers"
"github.com/Kshitij09/snakechat_server/transport/writer"
"net/http"
)

type FollowUserResponse struct {
Total int `json:"total"`
Follows []FollowUser `json:"follows"`
Offset *string `json:"offset,omitempty"`
}

type FollowListRequest struct {
Offset string `json:"offset"`
}

type FollowUser struct {
Id string `json:"id"`
Name string `json:"name"`
ProfileUrl *string `json:"profile_url,omitempty"`
UpdatedAt int64 `json:"updated_at"`
}

func FollowersHandler(db *sql.DB) handlers.Handler {
storage := sqlite.NewFollowsStorage(db)
service := domain.NewFollowService(storage)
return genericFollowersHandler(service.Followers)
}

func FollowingHandler(db *sql.DB) handlers.Handler {
storage := sqlite.NewFollowsStorage(db)
service := domain.NewFollowService(storage)
return genericFollowersHandler(service.Following)
}

type followsGetter func(id string, offset *string) (*paging.Page[int64, domain.FollowUser], error)

func genericFollowersHandler(getter followsGetter) handlers.Handler {
return func(w http.ResponseWriter, r *http.Request) error {
id := r.PathValue("id")
if id == "" {
return apierror.SimpleAPIError(http.StatusBadRequest, "id is missing in the path")
}

var (
followsPage *paging.Page[int64, domain.FollowUser]
err error
)
if r.Body == http.NoBody {
followsPage, err = getter(id, nil)
} else {
req := FollowListRequest{}
decodeErr := json.NewDecoder(r.Body).Decode(&req)
if decodeErr != nil {
return apierror.SimpleAPIError(http.StatusBadRequest, "Invalid request body")
}
followsPage, err = getter(id, &req.Offset)
}
if err != nil {
if errors.Is(err, offsetconv.ErrInvalidOffset) {
return apierror.SimpleAPIError(http.StatusBadRequest, "Invalid offset")
}
return err
}
resp := FollowUserResponse{
Total: len(followsPage.Items),
Follows: toTransportFollows(followsPage.Items),
Offset: followsPage.Offset,
}
return writer.SuccessJson(w, resp)
}
}

func toTransportFollows(follows []domain.FollowUser) []FollowUser {
followsTransport := make([]FollowUser, 0, len(follows))
for _, user := range follows {
u := FollowUser{
Id: user.Id,
Name: user.Name,
ProfileUrl: user.ProfileUrl,
UpdatedAt: user.UpdatedAt,
}
followsTransport = append(followsTransport, u)
}
return followsTransport
}
8 changes: 8 additions & 0 deletions backend/transport/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ func (s *Server) Run(port int, enableSsl bool) error {
userProfile = securedMiddleware(userProfile)
router.HandleFunc("POST /v1/user/{id}", handlers.NewHttpHandler(userProfile))

followers := FollowersHandler(db)
followers = securedMiddleware(followers)
router.HandleFunc("POST /v1/users/{id}/followers", handlers.NewHttpHandler(followers))

following := FollowingHandler(db)
following = securedMiddleware(following)
router.HandleFunc("POST /v1/users/{id}/following", handlers.NewHttpHandler(following))

postLikers := PostLikersHandler(db)
postLikers = securedMiddleware(postLikers)
router.HandleFunc("POST /v1/posts/{id}/likers", handlers.NewHttpHandler(postLikers))
Expand Down

0 comments on commit f57540c

Please sign in to comment.