From 948e036e8cbe5f42939782d65c6a3c0839c2a7d4 Mon Sep 17 00:00:00 2001 From: nihonium Date: Sun, 26 Oct 2025 02:34:45 +0300 Subject: [PATCH] feat: implemented /users/{id} api route --- api/api.gen.go | 123 ++--- api/openapi.yaml | 298 ++++++------- modules/backend/handlers.go | 719 ------------------------------ modules/backend/handlers/users.go | 51 +++ modules/backend/main.go | 3 +- modules/backend/queries.sql | 20 +- sql/migrations/000001_init.up.sql | 4 +- sql/models.go | 66 ++- sql/queries.sql.go | 64 ++- sql/sqlc.yaml | 15 +- 10 files changed, 381 insertions(+), 982 deletions(-) delete mode 100644 modules/backend/handlers.go create mode 100644 modules/backend/handlers/users.go diff --git a/api/api.gen.go b/api/api.gen.go index 58b5b53..24aebd3 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -8,28 +8,48 @@ import ( "encoding/json" "fmt" "net/http" + "time" "github.com/gin-gonic/gin" "github.com/oapi-codegen/runtime" strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" + openapi_types "github.com/oapi-codegen/runtime/types" ) -// Title defines model for Title. -type Title map[string]interface{} +// User defines model for User. +type User struct { + // AvatarId ID of the user avatar (references images table) + AvatarId *int64 `json:"avatar_id"` -// GetTitleParams defines parameters for GetTitle. -type GetTitleParams struct { - Query *string `form:"query,omitempty" json:"query,omitempty"` - Limit *int `form:"limit,omitempty" json:"limit,omitempty"` - Offset *int `form:"offset,omitempty" json:"offset,omitempty"` + // CreationDate Timestamp when the user was created + CreationDate time.Time `json:"creation_date"` + + // DispName Display name + DispName *string `json:"disp_name,omitempty"` + + // Id Unique user ID (primary key) + Id *int64 `json:"id,omitempty"` + + // Mail User email + Mail *openapi_types.Email `json:"mail,omitempty"` + + // Nickname Username (alphanumeric + _ or -) + Nickname string `json:"nickname"` + + // UserDesc User description + UserDesc *string `json:"user_desc,omitempty"` +} + +// GetUsersUserIdParams defines parameters for GetUsersUserId. +type GetUsersUserIdParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } // ServerInterface represents all server handlers. type ServerInterface interface { - // Get titles - // (GET /title) - GetTitle(c *gin.Context, params GetTitleParams) + // Get user info + // (GET /users/{user_id}) + GetUsersUserId(c *gin.Context, userId string, params GetUsersUserIdParams) } // ServerInterfaceWrapper converts contexts to parameters. @@ -41,37 +61,22 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) -// GetTitle operation middleware -func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { +// GetUsersUserId operation middleware +func (siw *ServerInterfaceWrapper) GetUsersUserId(c *gin.Context) { var err error + // ------------- Path parameter "user_id" ------------- + var userId string + + err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) + return + } + // Parameter object where we will unmarshal all parameters from the context - var params GetTitleParams - - // ------------- Optional query parameter "query" ------------- - - err = runtime.BindQueryParameter("form", true, false, "query", c.Request.URL.Query(), ¶ms.Query) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter query: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "limit" ------------- - - err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "offset" ------------- - - err = runtime.BindQueryParameter("form", true, false, "offset", c.Request.URL.Query(), ¶ms.Offset) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter offset: %w", err), http.StatusBadRequest) - return - } + var params GetUsersUserIdParams // ------------- Optional query parameter "fields" ------------- @@ -88,7 +93,7 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { } } - siw.Handler.GetTitle(c, params) + siw.Handler.GetUsersUserId(c, userId, params) } // GinServerOptions provides options for the Gin server. @@ -118,39 +123,40 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.GET(options.BaseURL+"/title", wrapper.GetTitle) + router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId) } -type GetTitleRequestObject struct { - Params GetTitleParams +type GetUsersUserIdRequestObject struct { + UserId string `json:"user_id"` + Params GetUsersUserIdParams } -type GetTitleResponseObject interface { - VisitGetTitleResponse(w http.ResponseWriter) error +type GetUsersUserIdResponseObject interface { + VisitGetUsersUserIdResponse(w http.ResponseWriter) error } -type GetTitle200JSONResponse []Title +type GetUsersUserId200JSONResponse User -func (response GetTitle200JSONResponse) VisitGetTitleResponse(w http.ResponseWriter) error { +func (response GetUsersUserId200JSONResponse) VisitGetUsersUserIdResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type GetTitle204Response struct { +type GetUsersUserId404Response struct { } -func (response GetTitle204Response) VisitGetTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(204) +func (response GetUsersUserId404Response) VisitGetUsersUserIdResponse(w http.ResponseWriter) error { + w.WriteHeader(404) return nil } // StrictServerInterface represents all server handlers. type StrictServerInterface interface { - // Get titles - // (GET /title) - GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error) + // Get user info + // (GET /users/{user_id}) + GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) } type StrictHandlerFunc = strictgin.StrictGinHandlerFunc @@ -165,17 +171,18 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } -// GetTitle operation middleware -func (sh *strictHandler) GetTitle(ctx *gin.Context, params GetTitleParams) { - var request GetTitleRequestObject +// GetUsersUserId operation middleware +func (sh *strictHandler) GetUsersUserId(ctx *gin.Context, userId string, params GetUsersUserIdParams) { + var request GetUsersUserIdRequestObject + request.UserId = userId request.Params = params handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetTitle(ctx, request.(GetTitleRequestObject)) + return sh.ssi.GetUsersUserId(ctx, request.(GetUsersUserIdRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetTitle") + handler = middleware(handler, "GetUsersUserId") } response, err := handler(ctx, request) @@ -183,8 +190,8 @@ func (sh *strictHandler) GetTitle(ctx *gin.Context, params GetTitleParams) { if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetTitleResponseObject); ok { - if err := validResponse.VisitGetTitleResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(GetUsersUserIdResponseObject); ok { + if err := validResponse.VisitGetUsersUserIdResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { diff --git a/api/openapi.yaml b/api/openapi.yaml index b2a2df0..97fa3a4 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -5,40 +5,40 @@ info: servers: - url: https://api.example.com paths: - /title: - get: - summary: Get titles - parameters: - - in: query - name: query - schema: - type: string - - in: query - name: limit - schema: - type: integer - default: 10 - - in: query - name: offset - schema: - type: integer - default: 0 - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: List of titles - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Title' - '204': - description: No titles found + # /title: + # get: + # summary: Get titles + # parameters: + # - in: query + # name: query + # schema: + # type: string + # - in: query + # name: limit + # schema: + # type: integer + # default: 10 + # - in: query + # name: offset + # schema: + # type: integer + # default: 0 + # - in: query + # name: fields + # schema: + # type: string + # default: all + # responses: + # '200': + # description: List of titles + # content: + # application/json: + # schema: + # type: array + # items: + # $ref: '#/components/schemas/Title' + # '204': + # description: No titles found # /title/{title_id}: # get: @@ -124,122 +124,122 @@ paths: # '204': # description: No reviews found -# /users/{user_id}: -# get: -# summary: Get user info -# parameters: -# - in: path -# name: user_id -# required: true -# schema: -# type: string -# - in: query -# name: fields -# schema: -# type: string -# default: all -# responses: -# '200': -# description: User info -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/User' -# '404': -# description: User not found + /users/{user_id}: + get: + summary: Get user info + parameters: + - in: path + name: user_id + required: true + schema: + type: string + - in: query + name: fields + schema: + type: string + default: all + responses: + '200': + description: User info + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User not found -# patch: -# summary: Update user -# parameters: -# - in: path -# name: user_id -# required: true -# schema: -# type: string -# requestBody: -# required: true -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/User' -# responses: -# '200': -# description: Update result -# content: -# application/json: -# schema: -# type: object -# properties: -# success: -# type: boolean -# error: -# type: string + # patch: + # summary: Update user + # parameters: + # - in: path + # name: user_id + # required: true + # schema: + # type: string + # requestBody: + # required: true + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/User' + # responses: + # '200': + # description: Update result + # content: + # application/json: + # schema: + # type: object + # properties: + # success: + # type: boolean + # error: + # type: string -# delete: -# summary: Delete user -# parameters: -# - in: path -# name: user_id -# required: true -# schema: -# type: string -# responses: -# '200': -# description: Delete result -# content: -# application/json: -# schema: -# type: object -# properties: -# success: -# type: boolean -# error: -# type: string + # delete: + # summary: Delete user + # parameters: + # - in: path + # name: user_id + # required: true + # schema: + # type: string + # responses: + # '200': + # description: Delete result + # content: + # application/json: + # schema: + # type: object + # properties: + # success: + # type: boolean + # error: + # type: string -# /users: -# get: -# summary: Search user -# parameters: -# - in: query -# name: query -# schema: -# type: string -# - in: query -# name: fields -# schema: -# type: string -# responses: -# '200': -# description: List of users -# content: -# application/json: -# schema: -# type: array -# items: -# $ref: '#/components/schemas/User' + # /users: + # get: + # summary: Search user + # parameters: + # - in: query + # name: query + # schema: + # type: string + # - in: query + # name: fields + # schema: + # type: string + # responses: + # '200': + # description: List of users + # content: + # application/json: + # schema: + # type: array + # items: + # $ref: '#/components/schemas/User' -# post: -# summary: Add new user -# requestBody: -# required: true -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/User' -# responses: -# '200': -# description: Add result -# content: -# application/json: -# schema: -# type: object -# properties: -# success: -# type: boolean -# error: -# type: string -# user_json: -# $ref: '#/components/schemas/User' + # post: + # summary: Add new user + # requestBody: + # required: true + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/User' + # responses: + # '200': + # description: Add result + # content: + # application/json: + # schema: + # type: object + # properties: + # success: + # type: boolean + # error: + # type: string + # user_json: + # $ref: '#/components/schemas/User' # /users/{user_id}/titles: # get: @@ -541,14 +541,14 @@ components: User: type: object properties: - user_id: + id: type: integer - format: int32 + format: int64 description: Unique user ID (primary key) example: 1 avatar_id: type: integer - format: int32 + format: int64 description: ID of the user avatar (references images table) nullable: true example: null diff --git a/modules/backend/handlers.go b/modules/backend/handlers.go deleted file mode 100644 index 366f298..0000000 --- a/modules/backend/handlers.go +++ /dev/null @@ -1,719 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "nyanimedb/modules/backend/db" - sqlc "nyanimedb/sql" - "strconv" - "time" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "golang.org/x/crypto/bcrypt" -) - -type Server struct { - db *sqlc.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), - } -} diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go new file mode 100644 index 0000000..b67153d --- /dev/null +++ b/modules/backend/handlers/users.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "context" + oapi "nyanimedb/api" + sqlc "nyanimedb/sql" + "strconv" + + "github.com/jackc/pgx/v5" + "github.com/oapi-codegen/runtime/types" +) + +type Server struct { + db *sqlc.Queries +} + +func NewServer(db *sqlc.Queries) Server { + return Server{db: db} +} + +func parseInt64(s string) (int32, error) { + i, err := strconv.ParseInt(s, 10, 64) + return int32(i), err +} + +func mapUser(u sqlc.GetUserByIDRow) oapi.User { + return oapi.User{ + AvatarId: u.AvatarID, + CreationDate: u.CreationDate, + DispName: u.DispName, + Id: &u.ID, + Mail: (*types.Email)(u.Mail), + Nickname: u.Nickname, + UserDesc: u.UserDesc, + } +} + +func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdRequestObject) (oapi.GetUsersUserIdResponseObject, error) { + userID, err := parseInt64(req.UserId) + if err != nil { + return oapi.GetUsersUserId404Response{}, nil + } + user, err := s.db.GetUserByID(context.TODO(), int64(userID)) + if err != nil { + if err == pgx.ErrNoRows { + return oapi.GetUsersUserId404Response{}, nil + } + return nil, err + } + return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil +} diff --git a/modules/backend/main.go b/modules/backend/main.go index a9e8db0..42a66d3 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -9,6 +9,7 @@ import ( "time" oapi "nyanimedb/api" + handlers "nyanimedb/modules/backend/handlers" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" @@ -42,7 +43,7 @@ func main() { queries := sqlc.New(conn) - server := NewServer(queries) + server := handlers.NewServer(queries) // r.LoadHTMLGlob("templates/*") r.Use(cors.New(cors.Config{ diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 7c9c197..b1dd8af 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -1,17 +1,17 @@ -- name: GetImageByID :one -SELECT image_id, storage_type, image_path +SELECT id, storage_type, image_path FROM images -WHERE image_id = $1; +WHERE id = $1; --- -- name: CreateImage :one --- INSERT INTO images (storage_type, image_path) --- VALUES ($1, $2) --- RETURNING image_id, storage_type, image_path; +-- name: CreateImage :one +INSERT INTO images (storage_type, image_path) +VALUES ($1, $2) +RETURNING id, storage_type, image_path; --- -- name: GetUserByID :one --- SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date --- FROM users --- WHERE user_id = $1; +-- name: GetUserByID :one +SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date +FROM users +WHERE id = $1; -- -- name: ListUsers :many -- SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index 00114a3..0b7fa33 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -25,7 +25,7 @@ CREATE TABLE images ( CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - avatar_id int REFERENCES images (id), + avatar_id bigint REFERENCES images (id), passhash text NOT NULL, mail text CHECK (mail ~ '[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+'), nickname text NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]+$'), @@ -38,7 +38,7 @@ CREATE TABLE users ( CREATE TABLE studios ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, studio_name text UNIQUE, - illust_id int REFERENCES images (id), + illust_id bigint REFERENCES images (id), studio_desc text ); diff --git a/sql/models.go b/sql/models.go index 4bed173..928d5ac 100644 --- a/sql/models.go +++ b/sql/models.go @@ -7,6 +7,7 @@ package sqlc import ( "database/sql/driver" "fmt" + "time" "github.com/jackc/pgx/v5/pgtype" ) @@ -185,48 +186,42 @@ func (ns NullUsertitleStatusT) Value() (driver.Value, error) { } type Image struct { - ImageID int32 `json:"image_id"` + ID int64 `json:"id"` StorageType StorageTypeT `json:"storage_type"` ImagePath string `json:"image_path"` } type Provider struct { - ProviderID int32 `json:"provider_id"` + ID int64 `json:"id"` ProviderName string `json:"provider_name"` -} - -type Review struct { - ReviewID int32 `json:"review_id"` - UserID int32 `json:"user_id"` - TitleID int32 `json:"title_id"` - ReviewText string `json:"review_text"` - CreationDate pgtype.Timestamp `json:"creation_date"` + Credentials []byte `json:"credentials"` } type Signal struct { - SignalID int32 `json:"signal_id"` + ID int64 `json:"id"` + TitleID *int64 `json:"title_id"` RawData []byte `json:"raw_data"` - ProviderID int32 `json:"provider_id"` - Dirty bool `json:"dirty"` + ProviderID int64 `json:"provider_id"` + Pending bool `json:"pending"` } type Studio struct { - StudioID int32 `json:"studio_id"` + ID int64 `json:"id"` StudioName *string `json:"studio_name"` - IllustID *int32 `json:"illust_id"` + IllustID *int64 `json:"illust_id"` StudioDesc *string `json:"studio_desc"` } type Tag struct { - TagID int32 `json:"tag_id"` + ID int64 `json:"id"` TagNames []byte `json:"tag_names"` } type Title struct { - TitleID int32 `json:"title_id"` + ID int64 `json:"id"` TitleNames []byte `json:"title_names"` - StudioID int32 `json:"studio_id"` - PosterID *int32 `json:"poster_id"` + StudioID int64 `json:"studio_id"` + PosterID *int64 `json:"poster_id"` TitleStatus TitleStatusT `json:"title_status"` Rating *float64 `json:"rating"` RatingCount *int32 `json:"rating_count"` @@ -239,26 +234,27 @@ type Title struct { } type TitleTag struct { - TitleID int32 `json:"title_id"` - TagID int32 `json:"tag_id"` + TitleID int64 `json:"title_id"` + TagID int64 `json:"tag_id"` } type User struct { - UserID int32 `json:"user_id"` - AvatarID *int32 `json:"avatar_id"` - Passhash string `json:"passhash"` - Mail *string `json:"mail"` - Nickname string `json:"nickname"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - CreationDate pgtype.Timestamp `json:"creation_date"` + ID int64 `json:"id"` + AvatarID *int64 `json:"avatar_id"` + Passhash string `json:"passhash"` + Mail *string `json:"mail"` + Nickname string `json:"nickname"` + DispName *string `json:"disp_name"` + UserDesc *string `json:"user_desc"` + CreationDate time.Time `json:"creation_date"` + LastLogin pgtype.Timestamptz `json:"last_login"` } type Usertitle struct { - UsertitleID int32 `json:"usertitle_id"` - UserID int32 `json:"user_id"` - TitleID int32 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int32 `json:"review_id"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` + Status UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + ReviewText *string `json:"review_text"` + ReviewDate pgtype.Timestamptz `json:"review_date"` } diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 2b00bef..8f92c2a 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -7,17 +7,67 @@ package sqlc import ( "context" + "time" ) -const getImageByID = `-- name: GetImageByID :one -SELECT image_id, storage_type, image_path -FROM images -WHERE image_id = $1 +const createImage = `-- name: CreateImage :one +INSERT INTO images (storage_type, image_path) +VALUES ($1, $2) +RETURNING id, storage_type, image_path ` -func (q *Queries) GetImageByID(ctx context.Context, imageID int32) (Image, error) { - row := q.db.QueryRow(ctx, getImageByID, imageID) +type CreateImageParams struct { + StorageType StorageTypeT `json:"storage_type"` + ImagePath string `json:"image_path"` +} + +func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Image, error) { + row := q.db.QueryRow(ctx, createImage, arg.StorageType, arg.ImagePath) var i Image - err := row.Scan(&i.ImageID, &i.StorageType, &i.ImagePath) + err := row.Scan(&i.ID, &i.StorageType, &i.ImagePath) + return i, err +} + +const getImageByID = `-- name: GetImageByID :one +SELECT id, storage_type, image_path +FROM images +WHERE id = $1 +` + +func (q *Queries) GetImageByID(ctx context.Context, id int64) (Image, error) { + row := q.db.QueryRow(ctx, getImageByID, id) + var i Image + err := row.Scan(&i.ID, &i.StorageType, &i.ImagePath) + return i, err +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date +FROM users +WHERE id = $1 +` + +type GetUserByIDRow struct { + ID int64 `json:"id"` + AvatarID *int64 `json:"avatar_id"` + Mail *string `json:"mail"` + Nickname string `json:"nickname"` + DispName *string `json:"disp_name"` + UserDesc *string `json:"user_desc"` + CreationDate time.Time `json:"creation_date"` +} + +func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error) { + row := q.db.QueryRow(ctx, getUserByID, id) + var i GetUserByIDRow + err := row.Scan( + &i.ID, + &i.AvatarID, + &i.Mail, + &i.Nickname, + &i.DispName, + &i.UserDesc, + &i.CreationDate, + ) return i, err } diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 23b22f9..f44761e 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -11,4 +11,17 @@ sql: sql_package: "pgx/v5" sql_driver: "github.com/jackc/pgx/v5" emit_json_tags: true - emit_pointers_for_null_types: true \ No newline at end of file + emit_pointers_for_null_types: true + overrides: + - db_type: "uuid" + nullable: false + go_type: + import: "github.com/gofrs/uuid" + package: "gofrsuuid" + type: UUID + pointer: true + - db_type: "timestamptz" + nullable: false + go_type: + import: "time" + type: "Time" \ No newline at end of file