Лента следует
Помимо простого добавления новых каналов в базу данных, пользователи могут указать, на какие каналы они хотят подписаться. Это будет важно позже, когда мы захотим показать пользователям список сообщений из каналов, на которые они подписаны.
Добавьте поддержку следующих конечных точек и обновите конечную точку «создать фид», как указано ниже.
Что такое «подписка на ленту»?
Подписка на ленту — это просто ссылка между пользователем и лентой. Это отношение многие ко многим, поэтому пользователь может следить за многими лентами, а за лентой могут следить многие пользователи.
Создание подписки на ленту указывает, что пользователь теперь следит за лентой. Удаление — это то же самое, что «отписаться» от фида.
Важно понимать, что ID
подписки на ленту — это не то же самое, что ID
самой ленты. Каждая пара «пользователь/лента» будет иметь уникальный идентификатор подписки на ленту.
Создать ленту подписки
Конечная точка: POST /v1/feed_follows
Требуется аутентификация
Пример тела запроса:
{ "feed_id": "4a82b372-b0e2-45e3-956a-b9b83358f86b" }
Пример тела ответа:
{ "id": "c834c69e-ee26-4c63-a677-a977432f9cfa", "feed_id": "4a82b372-b0e2-45e3-956a-b9b83358f86b", "user_id": "0e4fecc6-1354-47b8-8336-2077b307b20e", "created_at": "2017-01-01T00:00:00Z", "updated_at": "2017-01-01T00:00:00Z" }
Решения:
-- name: CreateFeedFollow :one INSERT INTO feed_follows (id, feed_id, user_id, created_at, updated_at) VALUES ( $1, $2, $3, $4, $5 ) RETURNING *; -- +goose Up CREATE TABLE feed_follows ( id UUID NOT NULL PRIMARY KEY, feed_id UUID NOT NULL REFERENCES feeds(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, UNIQUE (user_id, feed_id) ); -- +goose Down DROP TABLE feed_follows; package main import ( "encoding/json" "fmt" "net/http" "time" "github.com/google/uuid" "github.com/lordmoma/blog-aggregator/internal/database" ) type feedRequest struct { Name string `json:"name"` URL string `json:"url"` } type feedResponse struct { ID uuid.UUID `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Name string `json:"name"` URL string `json:"url"` UserID uuid.UUID `json:"user_id"` } type feedFollowRequest struct { FeedID uuid.UUID `json:"feed_id"` } type feedFollowResponse struct { ID uuid.UUID `json:"id"` FeedID uuid.UUID `json:"feed_id"` UserID uuid.UUID `json:"user_id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (apiCfg *apiConfig) createFeedHandler(w http.ResponseWriter, r *http.Request, user database.User) { var req feedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondWithError(w, http.StatusBadRequest, "Couldn't decode parameters") return } params := database.CreateFeedParams{ ID: uuid.New(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), Name: req.Name, Url: req.URL, UserID: user.ID, } feed, err := apiCfg.DB.CreateFeed(r.Context(), params) if err != nil { fmt.Println(err) respondWithError(w, http.StatusInternalServerError, "Couldn't create user") return } respondWithJSON(w, http.StatusOK, feedResponse{ ID: feed.ID, CreatedAt: feed.CreatedAt, UpdatedAt: feed.UpdatedAt, Name: feed.Name, URL: feed.Url, UserID: feed.UserID, }) } func (apiCfg *apiConfig) getFeedHandler(w http.ResponseWriter, r *http.Request) { feed, err := apiCfg.DB.GetFeeds(r.Context()) if err != nil { respondWithError(w, http.StatusInternalServerError, "Couldn't get feeds") return } respondWithJSON(w, http.StatusOK, feed) } func (apiCfg *apiConfig) createFeedFollowHandler(w http.ResponseWriter, r *http.Request, user database.User) { var req feedFollowRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondWithError(w, http.StatusBadRequest, "Couldn't decode parameters") return } params := database.CreateFeedFollowParams{ ID: uuid.New(), FeedID: req.FeedID, UserID: user.ID, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } feedFollow, err := apiCfg.DB.CreateFeedFollow(r.Context(), params) if err != nil { respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") return } respondWithJSON(w, http.StatusOK, feedFollowResponse{ ID: feedFollow.ID, FeedID: feedFollow.FeedID, UserID: feedFollow.UserID, CreatedAt: feedFollow.CreatedAt, UpdatedAt: feedFollow.UpdatedAt, }) }
Не забудьте проверить репо, если вы потерялись в структуре кода:
https://github.com/LordMoMA/блог-агрегатор
Удалить ленту подписки
Конечная точка: DELETE /v1/feed_follows/{feedFollowID}
-- name: DeleteFeedFollow :exec DELETE FROM feed_follows WHERE id = $1; func (apiCfg *apiConfig) deleteFeedFollowHandler(w http.ResponseWriter, r *http.Request, user database.User) { idString := chi.URLParam(r, "feedFollowID") fmt.Println(idString) id, err := uuid.Parse(idString) if err != nil { respondWithError(w, http.StatusBadRequest, "Couldn't parse feed_follow id") return } if err := apiCfg.DB.DeleteFeedFollow(r.Context(), id); err != nil { respondWithError(w, http.StatusInternalServerError, "Couldn't delete feed follow") return } respondWithJSON(w, http.StatusOK, nil) }
Получить все подписки на канал для пользователя
Конечная точка: GET /v1/feed_follows
Требуется аутентификация
Пример ответа:
[ { "id": "c834c69e-ee26-4c63-a677-a977432f9cfa", "feed_id": "4a82b372-b0e2-45e3-956a-b9b83358f86b", "user_id": "0e4fecc6-1354-47b8-8336-2077b307b20e", "created_at": "2017-01-01T00:00:00Z", "updated_at": "2017-01-01T00:00:00Z" }, { "id": "ad752167-f509-4ff3-8425-7781090b5c8f", "feed_id": "f71b842d-9fd1-4bc0-9913-dd96ba33bb15", "user_id": "0e4fecc6-1354-47b8-8336-2077b307b20e", "created_at": "2017-01-01T00:00:00Z", "updated_at": "2017-01-01T00:00:00Z" } ]
Решения:
-- name: CreateFeedFollow :one INSERT INTO feed_follows (id, feed_id, user_id, created_at, updated_at) VALUES ( $1, $2, $3, $4, $5 ) RETURNING *; -- name: DeleteFeedFollow :exec DELETE FROM feed_follows WHERE id = $1; -- name: GetFeedFollows :many SELECT * FROM feed_follows WHERE user_id = $1; func (apiCfg *apiConfig) getFeedFollowHandler(w http.ResponseWriter, r *http.Request, user database.User) { feedFollow, err := apiCfg.DB.GetFeedFollows(r.Context(), user.ID) if err != nil { respondWithError(w, http.StatusInternalServerError, "Couldn't get feed follows") return } respondWithJSON(w, http.StatusOK, feedFollow) }
Автоматически создавать фид подписки при создании фида
Когда пользователь создает новую ленту, он должен автоматически подписаться на эту ленту. Конечно, они могут отписаться от него позже, но он должен быть там по умолчанию.
Ответ этой конечной точки теперь должен содержать обе сущности:
{ "feed": { the feed object }, "feed_follow": { the feed follow object } }
Решения:
Нам нужно найти, где здесь логика, мы будем иметь дело с createFeedHandler
или createFeedFollowHandler
?
Мы знаем, что в нашей предыдущей реализации feed
и feed_follow
имели собственную структуру response
. Теперь мы хотим вернуть обе структуры одновременно, когда произойдет POST
`http://localhost:8080/v1/feeds`.
На данный момент указанная выше конечная точка возвращает только следующий ответ, и мы знаем, что идентификатор в JSON равен feed_id
. Чтобы вернуть обе структуры, нам нужно использовать этот feed_id
для выполнения POST
`http://localhost:8080/v1. /feed_follows`.
{ "id": "4fe179ef-e32a-4897-b903-4eeaf6c900ef", "created_at": "2023-04-08T09:17:31.318138Z", "updated_at": "2023-04-08T09:17:31.318138Z", "name": "huahua", "url": "https://canada/index.xml", "user_id": "1540ce78-6d90-41c4-beae-58bf97c47fad" }
Теперь логика понятнее, нам нужна сделка с createFeedHandler
и использовать часть createFeedFollowHandler
внутри нее.
type combineResponse struct { Feed feedResponse `json:"feed"` FeedFollow feedFollowResponse `json:"feed_follow"` } func (apiCfg *apiConfig) createFeedHandler(w http.ResponseWriter, r *http.Request, user database.User) { var req feedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondWithError(w, http.StatusBadRequest, "Couldn't decode parameters") return } params := database.CreateFeedParams{ ID: uuid.New(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), Name: req.Name, Url: req.URL, UserID: user.ID, } feed, err := apiCfg.DB.CreateFeed(r.Context(), params) if err != nil { fmt.Println(err) respondWithError(w, http.StatusInternalServerError, "Couldn't create user") return } params2 := database.CreateFeedFollowParams{ ID: uuid.New(), FeedID: feed.ID, UserID: user.ID, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } feedFollow, err := apiCfg.DB.CreateFeedFollow(r.Context(), params2) if err != nil { respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") return } respondWithJSON(w, http.StatusOK, combineResponse{ Feed: feedResponse{ ID: feed.ID, CreatedAt: feed.CreatedAt, UpdatedAt: feed.UpdatedAt, Name: feed.Name, URL: feed.Url, UserID: feed.UserID, }, FeedFollow: feedFollowResponse{ ID: feedFollow.ID, FeedID: feedFollow.FeedID, UserID: feedFollow.UserID, CreatedAt: feedFollow.CreatedAt, UpdatedAt: feedFollow.UpdatedAt, }, }) }