-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added followers/following APIs --------- Signed-off-by: Kshitij Patil <kshitijpatil98@gmail.com>
- Loading branch information
Showing
4 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters