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), } }