feat: implementation for handlers were written

This commit is contained in:
Iron_Felix 2025-10-11 02:13:08 +03:00
parent 31a09037cb
commit 80285ba7da
2 changed files with 2676 additions and 0 deletions

1958
modules/backend/api/gen.go Normal file

File diff suppressed because it is too large Load diff

718
modules/backend/api/impl.go Normal file
View file

@ -0,0 +1,718 @@
package api
import (
"context"
"encoding/json"
"nyanimedb-server/db"
"strconv"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt"
)
type Server struct {
db *db.Queries
}
func NewServer(db *db.Queries) Server {
return Server{db: db}
}
// —————————————————————————————————————————————
// ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
// —————————————————————————————————————————————
func parseInt32(s string) (int32, error) {
i, err := strconv.ParseInt(s, 10, 32)
return int32(i), err
}
func ptr[T any](v T) *T { return &v }
func pgInt4ToPtr(v pgtype.Int4) *int32 {
if v.Valid {
return &v.Int32
}
return nil
}
func pgTextToPtr(v pgtype.Text) *string {
if v.Valid {
return &v.String
}
return nil
}
func pgFloat8ToPtr(v pgtype.Float8) *float64 {
if v.Valid {
return &v.Float64
}
return nil
}
func jsonbToInterface(data []byte) interface{} {
if data == nil {
return nil
}
var out interface{}
if err := json.Unmarshal(data, &out); err != nil {
return string(data) // fallback
}
return out
}
// —————————————————————————————————————————————
// ХЕНДЛЕРЫ
// —————————————————————————————————————————————
func (s Server) GetMedia(ctx context.Context, req GetMediaRequestObject) (GetMediaResponseObject, error) {
id, err := parseInt32(req.Params.ImageId)
if err != nil {
return GetMedia200JSONResponse{Success: ptr(false), Error: ptr("invalid image_id")}, nil
}
img, err := s.db.GetImageByID(ctx, id)
if err != nil {
if err == pgx.ErrNoRows {
return GetMedia200JSONResponse{Success: ptr(false), Error: ptr("image not found")}, nil
}
return nil, err
}
return GetMedia200JSONResponse{
Success: ptr(true),
ImagePath: ptr(img.ImagePath),
}, nil
}
func (s Server) PostMedia(ctx context.Context, req PostMediaRequestObject) (PostMediaResponseObject, error) {
// ❗ Не реализовано: OpenAPI не определяет тело запроса для загрузки
return PostMedia200JSONResponse{
Success: ptr(false),
Error: ptr("upload not implemented: request body not defined in spec"),
}, nil
}
func (s Server) GetUsers(ctx context.Context, req GetUsersRequestObject) (GetUsersResponseObject, error) {
users, err := s.db.ListUsers(ctx, db.ListUsersParams{})
if err != nil {
return nil, err
}
var resp []User
for _, u := range users {
resp = append(resp, mapUser(u))
}
return GetUsers200JSONResponse(resp), nil
}
func (s Server) PostUsers(ctx context.Context, req PostUsersRequestObject) (PostUsersResponseObject, error) {
if req.Body == nil {
return PostUsers200JSONResponse{
Success: ptr(false),
Error: ptr("request body is required"),
}, nil
}
body := *req.Body
// Обязательные поля
nickname, ok := body["nickname"].(string)
if !ok || nickname == "" {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("nickname is required")}, nil
}
mail, ok := body["mail"].(string)
if !ok || mail == "" {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("mail is required")}, nil
}
password, ok := body["password"].(string)
if !ok || password == "" {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("password is required")}, nil
}
// Опциональные поля
var avatarID *int32
if v, ok := body["avatar_id"].(float64); ok {
id := int32(v)
avatarID = &id
}
dispName, _ := body["disp_name"].(string)
userDesc, _ := body["user_desc"].(string)
// 🔐 Хешируем пароль
passhashBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("failed to hash password")}, nil
}
passhash := string(passhashBytes)
// Сохраняем в БД
_, err = s.db.CreateUser(ctx, db.CreateUserParams{
AvatarID: pgtype.Int4{
Int32: 0,
Valid: avatarID != nil,
},
Passhash: passhash,
Mail: pgtype.Text{
String: mail,
Valid: true,
},
Nickname: nickname,
DispName: pgtype.Text{
String: dispName,
Valid: dispName != "",
},
UserDesc: pgtype.Text{
String: userDesc,
Valid: userDesc != "",
},
CreationDate: pgtype.Timestamp{
Time: time.Now(),
Valid: true,
},
})
if err != nil {
// Проверяем нарушение уникальности (например, дубль mail или nickname)
if err.Error() == "ERROR: duplicate key value violates unique constraint \"users_mail_key\"" ||
err.Error() == "ERROR: duplicate key value violates unique constraint \"users_nickname_key\"" {
return PostUsers200JSONResponse{
Success: ptr(false),
Error: ptr("user with this email or nickname already exists"),
}, nil
}
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("database error")}, nil
}
// Получаем созданного пользователя (без passhash и mail!)
// Предположим, что у вас есть запрос GetUserByNickname или аналогичный
// Но проще — вернуть только ID и nickname
// ⚠️ Поскольку мы не знаем user_id, можно:
// а) добавить RETURNING в CreateUser (рекомендуется),
// б) сделать отдельный SELECT.
// Пока вернём минимальный ответ
userResp := User{
"nickname": nickname,
// "user_id" можно добавить, если обновите query.sql
}
return PostUsers200JSONResponse{
Success: ptr(true),
UserJson: &userResp,
}, nil
}
func (s Server) DeleteUsersUserId(ctx context.Context, req DeleteUsersUserIdRequestObject) (DeleteUsersUserIdResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return DeleteUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
err = s.db.DeleteUser(ctx, userID)
if err != nil {
if err == pgx.ErrNoRows {
return DeleteUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("user not found")}, nil
}
return nil, err
}
return DeleteUsersUserId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetUsersUserId(ctx context.Context, req GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return GetUsersUserId404Response{}, nil
}
user, err := s.db.GetUserByID(ctx, userID)
if err != nil {
if err == pgx.ErrNoRows {
return GetUsersUserId404Response{}, nil
}
return nil, err
}
return GetUsersUserId200JSONResponse(mapUser(user)), nil
}
func (s Server) PatchUsersUserId(ctx context.Context, req PatchUsersUserIdRequestObject) (PatchUsersUserIdResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return PatchUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
if req.Body == nil {
return PatchUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
args := db.UpdateUserParams{
UserID: userID,
}
if v, ok := body["avatar_id"].(float64); ok {
args.AvatarID = pgtype.Int4{Int32: int32(v), Valid: true}
// args.AvatarIDValid = true
}
if v, ok := body["disp_name"].(string); ok {
args.DispName = pgtype.Text{String: v, Valid: true}
// args.DispNameValid = true
}
if v, ok := body["user_desc"].(string); ok {
args.UserDesc = pgtype.Text{String: v, Valid: true}
// args.UserDescValid = true
}
_, err = s.db.UpdateUser(ctx, args)
if err != nil {
return PatchUsersUserId200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PatchUsersUserId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetUsersUserIdReviews(ctx context.Context, req GetUsersUserIdReviewsRequestObject) (GetUsersUserIdReviewsResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return GetUsersUserIdReviews200JSONResponse{}, nil
}
limit := int32(20)
offset := int32(0)
// if req.Params.Limit != nil {
// limit = int32(*req.Params.Limit)
// }
// if req.Params.Offset != nil {
// offset = int32(*req.Params.Offset)
// }
reviews, err := s.db.ListReviewsByUser(ctx, db.ListReviewsByUserParams{
UserID: userID,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
var resp []Review
for _, r := range reviews {
resp = append(resp, mapReview(r))
}
return GetUsersUserIdReviews200JSONResponse(resp), nil
}
func (s Server) DeleteUsersUserIdTitles(ctx context.Context, req DeleteUsersUserIdTitlesRequestObject) (DeleteUsersUserIdTitlesResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return DeleteUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
if req.Params.TitleId != nil {
titleID, err := parseInt32(*req.Params.TitleId)
if err != nil {
return DeleteUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid title_id")}, nil
}
err = s.db.DeleteUserTitle(ctx, db.DeleteUserTitleParams{
UserID: userID,
Column2: titleID,
})
if err != nil && err != pgx.ErrNoRows {
return nil, err
}
}
// else {
// err = s.db.DeleteAllUserTitles(ctx, userID)
// if err != nil {
// return nil, err
// }
// }
return DeleteUsersUserIdTitles200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetUsersUserIdTitles(ctx context.Context, req GetUsersUserIdTitlesRequestObject) (GetUsersUserIdTitlesResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return GetUsersUserIdTitles200JSONResponse{}, nil
}
limit := int32(100)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
titles, err := s.db.ListUserTitles(ctx, db.ListUserTitlesParams{
UserID: userID,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
var resp []UserTitle
for _, t := range titles {
resp = append(resp, mapUserTitle(t))
}
return GetUsersUserIdTitles200JSONResponse(resp), nil
}
func (s Server) PatchUsersUserIdTitles(ctx context.Context, req PatchUsersUserIdTitlesRequestObject) (PatchUsersUserIdTitlesResponseObject, error) {
// userID, err := parseInt32(req.UserId)
// if err != nil {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
// }
// if req.Body == nil {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
// }
// body := *req.Body
// titleID, ok := body["title_id"].(float64)
// if !ok {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("title_id required")}, nil
// }
// args := db.UpdateUserTitleParams{
// UserID: userID,
// TitleID: int32(titleID),
// }
// if v, ok := body["status"].(string); ok {
// args.Status = db.UsertitleStatusT(v)
// // args.StatusValid = true
// }
// if v, ok := body["rate"].(float64); ok {
// args.Rate = pgtype.Int4{Int32: int32(v), Valid: true}
// // args.RateValid = true
// }
// if v, ok := body["review_id"].(float64); ok {
// args.ReviewID = pgtype.Int4{Int32: int32(v), Valid: true}
// // args.ReviewIDValid = true
// }
// _, err = s.db.UpdateUserTitle(ctx, args)
// if err != nil {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
// }
return PatchUsersUserIdTitles200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) PostUsersUserIdTitles(ctx context.Context, req PostUsersUserIdTitlesRequestObject) (PostUsersUserIdTitlesResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
if req.Body == nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := req.Body
titleID, err := parseInt32(*body.TitleId)
if err != nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid title_id")}, nil
}
status := db.UsertitleStatusT("planned")
if body.Status != nil {
status = db.UsertitleStatusT(*body.Status)
}
_, err = s.db.CreateUserTitle(ctx, db.CreateUserTitleParams{
UserID: userID,
TitleID: titleID,
Status: status,
Rate: pgtype.Int4{Valid: false},
ReviewID: pgtype.Int4{Valid: false},
})
if err != nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PostUsersUserIdTitles200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetTags(ctx context.Context, req GetTagsRequestObject) (GetTagsResponseObject, error) {
limit := int32(100)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
tags, err := s.db.ListTags(ctx, db.ListTagsParams{Limit: limit, Offset: offset})
if err != nil {
return nil, err
}
var resp []Tag
for _, t := range tags {
resp = append(resp, Tag{
"tag_id": t.TagID,
"tag_names": jsonbToInterface(t.TagNames),
})
}
return GetTags200JSONResponse(resp), nil
}
func (s Server) GetTitle(ctx context.Context, req GetTitleRequestObject) (GetTitleResponseObject, error) {
limit := int32(50)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
titles, err := s.db.ListTitles(ctx, db.ListTitlesParams{Limit: limit, Offset: offset})
if err != nil {
return nil, err
}
var resp []Title
for _, t := range titles {
resp = append(resp, mapTitle(t))
}
return GetTitle200JSONResponse(resp), nil
}
func (s Server) GetTitleTitleId(ctx context.Context, req GetTitleTitleIdRequestObject) (GetTitleTitleIdResponseObject, error) {
titleID, err := parseInt32(req.TitleId)
if err != nil {
return GetTitleTitleId404Response{}, nil
}
title, err := s.db.GetTitleByID(ctx, titleID)
if err != nil {
if err == pgx.ErrNoRows {
return GetTitleTitleId404Response{}, nil
}
return nil, err
}
return GetTitleTitleId200JSONResponse(mapTitle(title)), nil
}
func (s Server) PatchTitleTitleId(ctx context.Context, req PatchTitleTitleIdRequestObject) (PatchTitleTitleIdResponseObject, error) {
titleID, err := parseInt32(req.TitleId)
if err != nil {
return PatchTitleTitleId200JSONResponse{Success: ptr(false), Error: ptr("invalid title_id")}, nil
}
if req.Body == nil {
return PatchTitleTitleId200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
args := db.UpdateTitleParams{
TitleID: titleID,
}
if v, ok := body["title_names"].(map[string]interface{}); ok {
data, _ := json.Marshal(v)
args.TitleNames = data
// args.TitleNamesValid = true
}
if v, ok := body["studio_id"].(float64); ok {
args.StudioID = pgtype.Int4{Int32: int32(v), Valid: true}
// args.StudioIDValid = true
}
if v, ok := body["poster_id"].(float64); ok {
args.PosterID = pgtype.Int4{Int32: int32(v), Valid: true}
// args.PosterIDValid = true
}
// if v, ok := body["title_status"].(string); ok {
// args.TitleStatus = db.NullTitleStatusT(v)
// // args.TitleStatusValid = true
// }
if v, ok := body["release_year"].(float64); ok {
args.ReleaseYear = pgtype.Int4{Int32: int32(v), Valid: true}
// args.ReleaseYearValid = true
}
if v, ok := body["episodes_aired"].(float64); ok {
args.EpisodesAired = pgtype.Int4{Int32: int32(v), Valid: true}
// args.EpisodesAiredValid = true
}
if v, ok := body["episodes_all"].(float64); ok {
args.EpisodesAll = pgtype.Int4{Int32: int32(v), Valid: true}
// args.EpisodesAllValid = true
}
_, err = s.db.UpdateTitle(ctx, args)
if err != nil {
return PatchTitleTitleId200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PatchTitleTitleId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetTitleTitleIdReviews(ctx context.Context, req GetTitleTitleIdReviewsRequestObject) (GetTitleTitleIdReviewsResponseObject, error) {
titleID, err := parseInt32(req.TitleId)
if err != nil {
return GetTitleTitleIdReviews200JSONResponse{}, nil
}
limit := int32(20)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
reviews, err := s.db.ListReviewsByTitle(ctx, db.ListReviewsByTitleParams{
TitleID: titleID,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
var resp []Review
for _, r := range reviews {
resp = append(resp, mapReview(r))
}
return GetTitleTitleIdReviews200JSONResponse(resp), nil
}
func (s Server) PostReviews(ctx context.Context, req PostReviewsRequestObject) (PostReviewsResponseObject, error) {
if req.Body == nil {
return PostReviews200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
userID, ok1 := body["user_id"].(float64)
titleID, ok2 := body["title_id"].(float64)
reviewText, ok3 := body["review_text"].(string)
if !ok1 || !ok2 || !ok3 {
return PostReviews200JSONResponse{Success: ptr(false), Error: ptr("user_id, title_id, review_text required")}, nil
}
var imageIDs []int32
if ids, ok := body["image_ids"].([]interface{}); ok {
for _, id := range ids {
if f, ok := id.(float64); ok {
imageIDs = append(imageIDs, int32(f))
}
}
}
_, err := s.db.CreateReview(ctx, db.CreateReviewParams{
UserID: int32(userID),
TitleID: int32(titleID),
ImageIds: imageIDs,
ReviewText: reviewText,
CreationDate: pgtype.Timestamp{Time: time.Now(), Valid: true},
})
if err != nil {
return PostReviews200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PostReviews200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) DeleteReviewsReviewId(ctx context.Context, req DeleteReviewsReviewIdRequestObject) (DeleteReviewsReviewIdResponseObject, error) {
reviewID, err := parseInt32(req.ReviewId)
if err != nil {
return DeleteReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("invalid review_id")}, nil
}
err = s.db.DeleteReview(ctx, reviewID)
if err != nil {
if err == pgx.ErrNoRows {
return DeleteReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("review not found")}, nil
}
return nil, err
}
return DeleteReviewsReviewId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) PatchReviewsReviewId(ctx context.Context, req PatchReviewsReviewIdRequestObject) (PatchReviewsReviewIdResponseObject, error) {
reviewID, err := parseInt32(req.ReviewId)
if err != nil {
return PatchReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("invalid review_id")}, nil
}
if req.Body == nil {
return PatchReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
args := db.UpdateReviewParams{
ReviewID: reviewID,
}
if v, ok := body["review_text"].(string); ok {
args.ReviewText = pgtype.Text{String: v, Valid: true}
// args.ReviewTextValid = true
}
if ids, ok := body["image_ids"].([]interface{}); ok {
var imageIDs []int32
for _, id := range ids {
if f, ok := id.(float64); ok {
imageIDs = append(imageIDs, int32(f))
}
}
args.ImageIds = imageIDs
// args.ImageIdsValid = true
}
_, err = s.db.UpdateReview(ctx, args)
if err != nil {
return PatchReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PatchReviewsReviewId200JSONResponse{Success: ptr(true)}, nil
}
// —————————————————————————————————————————————
// МАППИНГИ
// —————————————————————————————————————————————
func mapUser(u db.Users) User {
return User{
"user_id": u.UserID,
"avatar_id": pgInt4ToPtr(u.AvatarID),
"nickname": u.Nickname,
"disp_name": pgTextToPtr(u.DispName),
"user_desc": pgTextToPtr(u.UserDesc),
"creation_date": u.CreationDate.Time,
// mail и passhash НЕ возвращаем!
}
}
func mapTitle(t db.Titles) Title {
var releaseSeason interface{}
if t.ReleaseSeason.Valid {
releaseSeason = string(t.ReleaseSeason.ReleaseSeasonT)
}
return Title{
"title_id": t.TitleID,
"title_names": jsonbToInterface(t.TitleNames),
"studio_id": t.StudioID,
"poster_id": pgInt4ToPtr(t.PosterID),
"signal_ids": t.SignalIds,
"title_status": string(t.TitleStatus),
"rating": pgFloat8ToPtr(t.Rating),
"rating_count": pgInt4ToPtr(t.RatingCount),
"release_year": pgInt4ToPtr(t.ReleaseYear),
"release_season": releaseSeason,
"season": pgInt4ToPtr(t.Season),
"episodes_aired": pgInt4ToPtr(t.EpisodesAired),
"episodes_all": pgInt4ToPtr(t.EpisodesAll),
"episodes_len": jsonbToInterface(t.EpisodesLen),
}
}
func mapReview(r db.Reviews) Review {
return Review{
"review_id": r.ReviewID,
"user_id": r.UserID,
"title_id": r.TitleID,
"image_ids": r.ImageIds,
"review_text": r.ReviewText,
"creation_date": r.CreationDate.Time,
}
}
func mapUserTitle(ut db.Usertitles) UserTitle {
return UserTitle{
"usertitle_id": ut.UsertitleID,
"user_id": ut.UserID,
"title_id": ut.TitleID,
"status": string(ut.Status),
"rate": pgInt4ToPtr(ut.Rate),
"review_id": pgInt4ToPtr(ut.ReviewID),
}
}