Feed следва

Освен самото добавяне на нови емисии към базата данни, потребителите могат да посочат кои емисии искат да следват. Това ще бъде важно по-късно, когато искаме да покажем на потребителите списък с публикации от емисиите, които следват.

Добавете поддръжка за следните крайни точки и актуализирайте крайната точка „създаване на канал“, както е посочено по-долу.

Какво е „следване на емисии“?

Последването на емисия е просто връзка между потребител и емисия. Това е връзка „много към много“, така че потребителят може да следва много емисии, а една емисия може да бъде следвана от много потребители.

Създаването на проследяване на емисия показва, че потребител вече следва емисия. Изтриването му е същото като „прекратяване на следенето“ на емисия.

Важно е да разберете, че 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/blog-aggregator

Изтриване на проследяване на емисия

Крайна точка: 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,
  },
 })
}