From e18ab0aa6edccf8ba4e63a1fceee4c19ef770566 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 9 Sep 2023 14:39:32 +0300 Subject: [PATCH] add feed follows --- handler_feed_follows.go | 71 +++++++++++++++++++++ http-requests/delete-feed-follow.http | 3 + http-requests/follow-feed.http | 7 +++ http-requests/list-feed-follows.http | 3 + internal/database/feed_follows.sql.go | 89 +++++++++++++++++++++++++++ internal/database/models.go | 8 +++ main.go | 4 ++ models.go | 26 ++++++++ sql/queries/feed_follows.sql | 10 +++ sql/schema/004_feed_follows.sql | 12 ++++ 10 files changed, 233 insertions(+) create mode 100644 handler_feed_follows.go create mode 100644 http-requests/delete-feed-follow.http create mode 100644 http-requests/follow-feed.http create mode 100644 http-requests/list-feed-follows.http create mode 100644 internal/database/feed_follows.sql.go create mode 100644 sql/queries/feed_follows.sql create mode 100644 sql/schema/004_feed_follows.sql diff --git a/handler_feed_follows.go b/handler_feed_follows.go new file mode 100644 index 0000000..fe8f5cb --- /dev/null +++ b/handler_feed_follows.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "git.rpuzonas.com/rpuzonas/go-rss-aggregator/internal/database" + "github.com/go-chi/chi" +) + + +func (apiCfg *apiConfig) handlerFollowFeed(w http.ResponseWriter, r *http.Request, user User) { + type parameters struct { + FeedID int64 `json:"feed_id"` + } + + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, 400, fmt.Sprintf("Error parsing JSON: %v", err)) + return + } + + now := time.Now().UTC() + feedFollow, err := apiCfg.DB.CreateFeedFollow(r.Context(), database.CreateFeedFollowParams{ + UpdatedAt: now, + CreatedAt: now, + UserID: user.ID, + FeedID: params.FeedID, + }) + if err != nil { + respondWithError(w, 400, fmt.Sprintf("Couldn't follow feed: %v", err)) + return + } + + respondWithJSON(w, 200, databaseFeedFollowToFeedFollow(feedFollow)) +} + +func (apiCfg *apiConfig) handlerGetFeedFollows(w http.ResponseWriter, r *http.Request, user User) { + feedFollows, err := apiCfg.DB.GetFeedFollowsByUser(r.Context(), user.ID) + if err != nil { + respondWithError(w, 400, fmt.Sprintf("Couldn't list feed follows: %v", err)) + return + } + + respondWithJSON(w, 200, databaseFolllowFeedsToFollowFeeds(feedFollows)) +} + +func (apiCfg *apiConfig) handlerDeleteFeedFollow(w http.ResponseWriter, r *http.Request, user User) { + feedFollowIDStr := chi.URLParam(r, "feedFollowID"); + feedFollowID, err := strconv.ParseInt(feedFollowIDStr, 10, 64) + if err != nil { + respondWithError(w, 400, fmt.Sprintf("Failed to parse feed id: %v", err)) + return + } + + err = apiCfg.DB.DeleteFeedFollow(r.Context(), database.DeleteFeedFollowParams{ + UserID: user.ID, + ID: feedFollowID, + }) + if err != nil { + respondWithError(w, 400, fmt.Sprintf("Failed to delete feed follow: %v", err)) + return + } + + respondWithJSON(w, 200, struct{}{}) +} diff --git a/http-requests/delete-feed-follow.http b/http-requests/delete-feed-follow.http new file mode 100644 index 0000000..602bf57 --- /dev/null +++ b/http-requests/delete-feed-follow.http @@ -0,0 +1,3 @@ +DELETE http://localhost:8080/v1/feed-follows/10 +Content-Type: application/json +Authorization: ApiKey aec0a1cbc6658af07e0d3ed67079a46928744b7787170a913abea4c68e9af702bbf2593bb8283b1574cf7b2e0d60df1abbd3e619c80e483d666f219ea71c72374e66b372a173e1407087c5cdeecf7dc66f34cdf59a0b6c448a86b6954ffc2693185550cd203625c8fa1eb0388e0834ee70cecec0dc199197a6e7229d28659fb6929c8410d74f0336a73ab62be5486451a270bcdbc37b0f73cad6654bc057fe28754e151330bbbc22c9e8d645015b81f035f95ec5b21b064965b65123f3a0705df4ab83a963d6a3353226cb7e9eeecdfb9631299e3068f9e28220e58bd7a69389a8697bee67501017cbde6fb2c809b136496533f40efed60be51e2b44899b8ade diff --git a/http-requests/follow-feed.http b/http-requests/follow-feed.http new file mode 100644 index 0000000..ea360f0 --- /dev/null +++ b/http-requests/follow-feed.http @@ -0,0 +1,7 @@ +POST http://localhost:8080/v1/feed-follows +Content-Type: application/json +Authorization: ApiKey aec0a1cbc6658af07e0d3ed67079a46928744b7787170a913abea4c68e9af702bbf2593bb8283b1574cf7b2e0d60df1abbd3e619c80e483d666f219ea71c72374e66b372a173e1407087c5cdeecf7dc66f34cdf59a0b6c448a86b6954ffc2693185550cd203625c8fa1eb0388e0834ee70cecec0dc199197a6e7229d28659fb6929c8410d74f0336a73ab62be5486451a270bcdbc37b0f73cad6654bc057fe28754e151330bbbc22c9e8d645015b81f035f95ec5b21b064965b65123f3a0705df4ab83a963d6a3353226cb7e9eeecdfb9631299e3068f9e28220e58bd7a69389a8697bee67501017cbde6fb2c809b136496533f40efed60be51e2b44899b8ade + +{ + "feed_id": 4 +} diff --git a/http-requests/list-feed-follows.http b/http-requests/list-feed-follows.http new file mode 100644 index 0000000..d932877 --- /dev/null +++ b/http-requests/list-feed-follows.http @@ -0,0 +1,3 @@ +GET http://localhost:8080/v1/feed-follows +Content-Type: application/json +Authorization: ApiKey aec0a1cbc6658af07e0d3ed67079a46928744b7787170a913abea4c68e9af702bbf2593bb8283b1574cf7b2e0d60df1abbd3e619c80e483d666f219ea71c72374e66b372a173e1407087c5cdeecf7dc66f34cdf59a0b6c448a86b6954ffc2693185550cd203625c8fa1eb0388e0834ee70cecec0dc199197a6e7229d28659fb6929c8410d74f0336a73ab62be5486451a270bcdbc37b0f73cad6654bc057fe28754e151330bbbc22c9e8d645015b81f035f95ec5b21b064965b65123f3a0705df4ab83a963d6a3353226cb7e9eeecdfb9631299e3068f9e28220e58bd7a69389a8697bee67501017cbde6fb2c809b136496533f40efed60be51e2b44899b8ade diff --git a/internal/database/feed_follows.sql.go b/internal/database/feed_follows.sql.go new file mode 100644 index 0000000..0fdf1d3 --- /dev/null +++ b/internal/database/feed_follows.sql.go @@ -0,0 +1,89 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 +// source: feed_follows.sql + +package database + +import ( + "context" + "time" +) + +const createFeedFollow = `-- name: CreateFeedFollow :one +INSERT INTO feed_follows (created_at, updated_at, user_id, feed_id) +VALUES (?, ?, ?, ?) +RETURNING id, created_at, updated_at, user_id, feed_id +` + +type CreateFeedFollowParams struct { + CreatedAt time.Time + UpdatedAt time.Time + UserID int64 + FeedID int64 +} + +func (q *Queries) CreateFeedFollow(ctx context.Context, arg CreateFeedFollowParams) (FeedFollow, error) { + row := q.db.QueryRowContext(ctx, createFeedFollow, + arg.CreatedAt, + arg.UpdatedAt, + arg.UserID, + arg.FeedID, + ) + var i FeedFollow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ) + return i, err +} + +const deleteFeedFollow = `-- name: DeleteFeedFollow :exec +DELETE FROM feed_follows WHERE id = ? AND user_id = ? +` + +type DeleteFeedFollowParams struct { + ID int64 + UserID int64 +} + +func (q *Queries) DeleteFeedFollow(ctx context.Context, arg DeleteFeedFollowParams) error { + _, err := q.db.ExecContext(ctx, deleteFeedFollow, arg.ID, arg.UserID) + return err +} + +const getFeedFollowsByUser = `-- name: GetFeedFollowsByUser :many +SELECT id, created_at, updated_at, user_id, feed_id FROM feed_follows WHERE user_id = ? +` + +func (q *Queries) GetFeedFollowsByUser(ctx context.Context, userID int64) ([]FeedFollow, error) { + rows, err := q.db.QueryContext(ctx, getFeedFollowsByUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FeedFollow + for rows.Next() { + var i FeedFollow + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/database/models.go b/internal/database/models.go index 50670a3..f610d2b 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -18,6 +18,14 @@ type Feed struct { UserID int64 } +type FeedFollow struct { + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + UserID int64 + FeedID int64 +} + type User struct { ID int64 CreatedAt time.Time diff --git a/main.go b/main.go index d2d7766..5b4a45b 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,10 @@ func createV1Router(cfg *apiConfig) chi.Router { v1Router.Post("/feeds", cfg.middlewareAuth(cfg.handlerCreateFeed)) v1Router.Get("/feeds", cfg.handlerGetAllFeeds) + v1Router.Post("/feed-follows", cfg.middlewareAuth(cfg.handlerFollowFeed)) + v1Router.Delete("/feed-follows/{feedFollowID}", cfg.middlewareAuth(cfg.handlerDeleteFeedFollow)) + v1Router.Get("/feed-follows", cfg.middlewareAuth(cfg.handlerGetFeedFollows)) + return v1Router } diff --git a/models.go b/models.go index 1887a39..68c1f2f 100644 --- a/models.go +++ b/models.go @@ -23,6 +23,14 @@ type Feed struct { UserID int64 `json:"user_id"` } +type FeedFollow struct { + ID int64 `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserID int64 `json:"user_id"` + FeedID int64 `json:"feed_id"` +} + func databaseUserToUser(dbUser database.User) User { return User{ ID: dbUser.ID, @@ -51,3 +59,21 @@ func databaseFeedsToFeeds(dbFeeds []database.Feed) []Feed { } return feeds } + +func databaseFeedFollowToFeedFollow(dbFeedFollow database.FeedFollow) FeedFollow { + return FeedFollow{ + ID: dbFeedFollow.ID, + CreatedAt: dbFeedFollow.CreatedAt, + UpdatedAt: dbFeedFollow.UpdatedAt, + UserID: dbFeedFollow.UserID, + FeedID: dbFeedFollow.FeedID, + } +} + +func databaseFolllowFeedsToFollowFeeds(dbFeedFollows []database.FeedFollow) []FeedFollow { + feeds := make([]FeedFollow, len(dbFeedFollows)) + for i, dbFeed := range dbFeedFollows { + feeds[i] = databaseFeedFollowToFeedFollow(dbFeed) + } + return feeds +} diff --git a/sql/queries/feed_follows.sql b/sql/queries/feed_follows.sql new file mode 100644 index 0000000..0716742 --- /dev/null +++ b/sql/queries/feed_follows.sql @@ -0,0 +1,10 @@ +-- name: CreateFeedFollow :one +INSERT INTO feed_follows (created_at, updated_at, user_id, feed_id) +VALUES (?, ?, ?, ?) +RETURNING *; + +-- name: GetFeedFollowsByUser :many +SELECT * FROM feed_follows WHERE user_id = ?; + +-- name: DeleteFeedFollow :exec +DELETE FROM feed_follows WHERE id = ? AND user_id = ?; diff --git a/sql/schema/004_feed_follows.sql b/sql/schema/004_feed_follows.sql new file mode 100644 index 0000000..d4010bc --- /dev/null +++ b/sql/schema/004_feed_follows.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feed_follows ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATE NOT NULL, + updated_at DATE NOT NULL, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + feed_id INTEGER NOT NULL REFERENCES feeds(id) ON DELETE CASCADE, + UNIQUE(user_id, feed_id) +); + +-- +goose Down +DROP TABLE feed_follows;