Лента следует

Помимо простого добавления новых каналов в базу данных, пользователи могут указать, на какие каналы они хотят подписаться. Это будет важно позже, когда мы захотим показать пользователям список сообщений из каналов, на которые они подписаны.

Добавьте поддержку следующих конечных точек и обновите конечную точку «создать фид», как указано ниже.

Что такое «подписка на ленту»?

Подписка на ленту — это просто ссылка между пользователем и лентой. Это отношение многие ко многим, поэтому пользователь может следить за многими лентами, а за лентой могут следить многие пользователи.

Создание подписки на ленту указывает, что пользователь теперь следит за лентой. Удаление — это то же самое, что «отписаться» от фида.

Важно понимать, что 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,
  },
 })
}