From b7b7a7d55d46a19831c73bdfa6a2eaa2fe395a41 Mon Sep 17 00:00:00 2001 From: Darius Rapalis Date: Tue, 11 Mar 2025 14:53:10 +0200 Subject: [PATCH] #9 - feed follows --- internal/database/createFeedFollow.sql.go | 73 +++++++++++++++++++ internal/database/getFeedByUrl.sql.go | 28 +++++++ internal/database/getFeedFollowsByUser.sql.go | 65 +++++++++++++++++ internal/database/models.go | 8 ++ internal/handlers/addFeedHandler.go | 13 +++- internal/handlers/feedFollowsByUserHandler.go | 27 +++++++ internal/handlers/followFeedHandler.go | 66 +++++++++++++++++ main.go | 2 + sql/queries/createFeedFollow.sql | 20 +++++ sql/queries/getFeedByUrl.sql | 2 + sql/queries/getFeedFollowsByUser.sql | 9 +++ sql/schema/0003_feed_follows.sql | 12 +++ 12 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 internal/database/createFeedFollow.sql.go create mode 100644 internal/database/getFeedByUrl.sql.go create mode 100644 internal/database/getFeedFollowsByUser.sql.go create mode 100644 internal/handlers/feedFollowsByUserHandler.go create mode 100644 internal/handlers/followFeedHandler.go create mode 100644 sql/queries/createFeedFollow.sql create mode 100644 sql/queries/getFeedByUrl.sql create mode 100644 sql/queries/getFeedFollowsByUser.sql create mode 100644 sql/schema/0003_feed_follows.sql diff --git a/internal/database/createFeedFollow.sql.go b/internal/database/createFeedFollow.sql.go new file mode 100644 index 0000000..4fe4d5b --- /dev/null +++ b/internal/database/createFeedFollow.sql.go @@ -0,0 +1,73 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: createFeedFollow.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeedFollow = `-- name: CreateFeedFollow :one + +WITH inserted_feed_follow AS ( + INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) + VALUES ( + $1, + $2, + $3, + $4, + $5 + ) + RETURNING id, created_at, updated_at, user_id, feed_id +) SELECT + inserted_feed_follow.id, inserted_feed_follow.created_at, inserted_feed_follow.updated_at, inserted_feed_follow.user_id, inserted_feed_follow.feed_id, + feeds.name AS feed_name, + users.name AS user_name +FROM inserted_feed_follow + INNER JOIN users ON users.id = inserted_feed_follow.user_id + INNER JOIN feeds ON feeds.id = inserted_feed_follow.feed_id +` + +type CreateFeedFollowParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + +type CreateFeedFollowRow struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID + FeedName string + UserName string +} + +func (q *Queries) CreateFeedFollow(ctx context.Context, arg CreateFeedFollowParams) (CreateFeedFollowRow, error) { + row := q.db.QueryRowContext(ctx, createFeedFollow, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.UserID, + arg.FeedID, + ) + var i CreateFeedFollowRow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + &i.FeedName, + &i.UserName, + ) + return i, err +} diff --git a/internal/database/getFeedByUrl.sql.go b/internal/database/getFeedByUrl.sql.go new file mode 100644 index 0000000..94e69fd --- /dev/null +++ b/internal/database/getFeedByUrl.sql.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: getFeedByUrl.sql + +package database + +import ( + "context" +) + +const getFeedByUrl = `-- name: GetFeedByUrl :one +SELECT id, created_at, updated_at, name, url, user_id FROM feeds WHERE url = $1 +` + +func (q *Queries) GetFeedByUrl(ctx context.Context, url string) (Feed, error) { + row := q.db.QueryRowContext(ctx, getFeedByUrl, url) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + ) + return i, err +} diff --git a/internal/database/getFeedFollowsByUser.sql.go b/internal/database/getFeedFollowsByUser.sql.go new file mode 100644 index 0000000..08401b3 --- /dev/null +++ b/internal/database/getFeedFollowsByUser.sql.go @@ -0,0 +1,65 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: getFeedFollowsByUser.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const getFeedFollowsByUser = `-- name: GetFeedFollowsByUser :many +SELECT + feed_follows.id, feed_follows.created_at, feed_follows.updated_at, feed_follows.user_id, feed_follows.feed_id, + feeds.name AS feed_name, + users.name AS user_name +FROM feed_follows + INNER JOIN users ON users.id = feed_follows.user_id + INNER JOIN feeds ON feeds.id = feed_follows.feed_id +WHERE feed_follows.user_id = $1 +` + +type GetFeedFollowsByUserRow struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID + FeedName string + UserName string +} + +func (q *Queries) GetFeedFollowsByUser(ctx context.Context, userID uuid.UUID) ([]GetFeedFollowsByUserRow, error) { + rows, err := q.db.QueryContext(ctx, getFeedFollowsByUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFeedFollowsByUserRow + for rows.Next() { + var i GetFeedFollowsByUserRow + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + &i.FeedName, + &i.UserName, + ); 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 6164490..40c75a3 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -19,6 +19,14 @@ type Feed struct { UserID uuid.UUID } +type FeedFollow struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + type User struct { ID uuid.UUID CreatedAt time.Time diff --git a/internal/handlers/addFeedHandler.go b/internal/handlers/addFeedHandler.go index 4222d45..304fe60 100644 --- a/internal/handlers/addFeedHandler.go +++ b/internal/handlers/addFeedHandler.go @@ -12,7 +12,7 @@ import ( "time" ) -func AddFeedHandler(s *config.State, cmd config.Command) error { +func AddFeedHandler(s *config.State, cmd config.Command, user database.User) error { if len(cmd.Args) < 2 { log.Fatal("usage: boot-dev-blog-aggregator addfeed NAME URL") @@ -45,6 +45,17 @@ func AddFeedHandler(s *config.State, cmd config.Command) error { return err } + _, err = s.DB.CreateFeedFollow(ctx, database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + UserID: user.ID, + FeedID: f.ID, + }) + if err != nil { + return err + } + fmt.Printf("%v", f) return nil diff --git a/internal/handlers/feedFollowsByUserHandler.go b/internal/handlers/feedFollowsByUserHandler.go new file mode 100644 index 0000000..c3d51f3 --- /dev/null +++ b/internal/handlers/feedFollowsByUserHandler.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "context" + "fmt" + "github.com/rdarius/boot-dev-blog-aggregator/internal/config" +) + +func GetFeedFollowsByUserHandler(s *config.State, cmd config.Command) error { + ctx := context.Background() + user, err := s.DB.GetUser(ctx, s.Config.CurrentUserName) + if err != nil { + return err + } + + ff, err := s.DB.GetFeedFollowsByUser(ctx, user.ID) + + if err != nil { + return err + } + + for _, feed := range ff { + fmt.Printf("* %s\n", feed.FeedName) + } + + return nil +} diff --git a/internal/handlers/followFeedHandler.go b/internal/handlers/followFeedHandler.go new file mode 100644 index 0000000..b7a69a8 --- /dev/null +++ b/internal/handlers/followFeedHandler.go @@ -0,0 +1,66 @@ +package handlers + +import ( + "context" + "fmt" + "github.com/google/uuid" + "github.com/rdarius/boot-dev-blog-aggregator/internal/config" + "github.com/rdarius/boot-dev-blog-aggregator/internal/database" + "github.com/rdarius/boot-dev-blog-aggregator/internal/rss" + "log" + "os" + "time" +) + +func FollowFeedHandler(s *config.State, cmd config.Command) error { + if len(cmd.Args) < 1 { + log.Fatal("usage: boot-dev-blog-aggregator follow URL") + } + ctx := context.Background() + user, err := s.DB.GetUser(ctx, s.Config.CurrentUserName) + if err != nil { + return err + } + + url := cmd.Args[0] + + ff, err := s.DB.GetFeedByUrl(ctx, url) + if err != nil { + // no feed, get it and save it + + fd, err := rss.FetchFeed(ctx, url) + if err != nil { + fmt.Printf("Failed to fetch feed: %v\n", err) + os.Exit(1) + } + + res, err := s.DB.CreateFeed(ctx, database.CreateFeedParams{ + ID: uuid.New(), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + UserID: user.ID, + Url: url, + Name: fd.Channel.Title, + }) + if err != nil { + fmt.Printf("Failed to create feed: %v\n", err) + os.Exit(1) + } + ff = res + } + + feed, err := s.DB.CreateFeedFollow(ctx, database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + UserID: user.ID, + FeedID: ff.ID, + }) + if err != nil { + return err + } + + fmt.Printf("%v", feed) + + return nil +} diff --git a/main.go b/main.go index 1f2353a..774987f 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,8 @@ func main() { commands.Register("agg", handlers.FetchFeedHandler) commands.Register("addfeed", handlers.AddFeedHandler) commands.Register("feeds", handlers.ListFeedsHandler) + commands.Register("follow", handlers.FollowFeedHandler) + commands.Register("following", handlers.GetFeedFollowsByUserHandler) if len(os.Args) < 2 { log.Fatal("usage: boot-dev-blog-aggregator [args...]") diff --git a/sql/queries/createFeedFollow.sql b/sql/queries/createFeedFollow.sql new file mode 100644 index 0000000..47f6ef8 --- /dev/null +++ b/sql/queries/createFeedFollow.sql @@ -0,0 +1,20 @@ +-- name: CreateFeedFollow :one + +WITH inserted_feed_follow AS ( + INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) + VALUES ( + $1, + $2, + $3, + $4, + $5 + ) + RETURNING * +) SELECT + inserted_feed_follow.*, + feeds.name AS feed_name, + users.name AS user_name +FROM inserted_feed_follow + INNER JOIN users ON users.id = inserted_feed_follow.user_id + INNER JOIN feeds ON feeds.id = inserted_feed_follow.feed_id; + diff --git a/sql/queries/getFeedByUrl.sql b/sql/queries/getFeedByUrl.sql new file mode 100644 index 0000000..f525780 --- /dev/null +++ b/sql/queries/getFeedByUrl.sql @@ -0,0 +1,2 @@ +-- name: GetFeedByUrl :one +SELECT * FROM feeds WHERE url = $1; \ No newline at end of file diff --git a/sql/queries/getFeedFollowsByUser.sql b/sql/queries/getFeedFollowsByUser.sql new file mode 100644 index 0000000..c2d0f61 --- /dev/null +++ b/sql/queries/getFeedFollowsByUser.sql @@ -0,0 +1,9 @@ +-- name: GetFeedFollowsByUser :many +SELECT + feed_follows.*, + feeds.name AS feed_name, + users.name AS user_name +FROM feed_follows + INNER JOIN users ON users.id = feed_follows.user_id + INNER JOIN feeds ON feeds.id = feed_follows.feed_id +WHERE feed_follows.user_id = $1; \ No newline at end of file diff --git a/sql/schema/0003_feed_follows.sql b/sql/schema/0003_feed_follows.sql new file mode 100644 index 0000000..c963100 --- /dev/null +++ b/sql/schema/0003_feed_follows.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feed_follows( + id UUID PRIMARY KEY, + created_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP not null, + user_id UUID not null REFERENCES users(id) ON DELETE CASCADE, + feed_id UUID not null REFERENCES feeds(id) ON DELETE CASCADE, + CONSTRAINT UC_User_Feed UNIQUE(user_id, feed_id) +); + +-- +goose Down +DROP TABLE feeds; \ No newline at end of file