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/modules/frontend/package-lock.json b/modules/frontend/package-lock.json
index b1789ed..6a06afb 100644
--- a/modules/frontend/package-lock.json
+++ b/modules/frontend/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"axios": "^1.12.2",
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router-dom": "^7.9.4"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@@ -2061,6 +2062,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3346,6 +3356,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3363,6 +3374,44 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
+ "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
+ "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.9.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3466,6 +3515,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/modules/frontend/package.json b/modules/frontend/package.json
index c40ff17..b4977aa 100644
--- a/modules/frontend/package.json
+++ b/modules/frontend/package.json
@@ -12,7 +12,8 @@
"dependencies": {
"axios": "^1.12.2",
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router-dom": "^7.9.4"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx
index 6b2ee5f..a88ad57 100644
--- a/modules/frontend/src/App.tsx
+++ b/modules/frontend/src/App.tsx
@@ -1,8 +1,15 @@
import React from "react";
+import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UserPage from "./components/UserPage/UserPage";
const App: React.FC = () => {
- return ;
+ return (
+
+
+ } />
+
+
+ );
};
-export default App;
+export default App;
\ No newline at end of file
diff --git a/modules/frontend/src/components/UserPage/UserPage.tsx b/modules/frontend/src/components/UserPage/UserPage.tsx
index b0db90c..0a83679 100644
--- a/modules/frontend/src/components/UserPage/UserPage.tsx
+++ b/modules/frontend/src/components/UserPage/UserPage.tsx
@@ -1,17 +1,21 @@
import React, { useEffect, useState } from "react";
-import { DefaultService } from "../../api/services/DefaultService"; // adjust path
+import { useParams } from "react-router-dom"; // <-- import
+import { DefaultService } from "../../api/services/DefaultService";
import type { User } from "../../api/models/User";
import styles from "./UserPage.module.css";
const UserPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>(); // <-- get user id from URL
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
+ if (!id) return;
+
const getUserInfo = async () => {
try {
- const userInfo = await DefaultService.getUsers("1", "all");
+ const userInfo = await DefaultService.getUsers(id, "all"); // <-- use dynamic id
setUser(userInfo);
} catch (err) {
console.error(err);
@@ -21,7 +25,7 @@ const UserPage: React.FC = () => {
}
};
getUserInfo();
- }, []);
+ }, [id]);
if (loading) return Loading...
;
if (error) return {error}
;
diff --git a/sql/migrations/000001_init.down.sql b/sql/migrations/000001_init.down.sql
index d9f23c0..dc52d23 100644
--- a/sql/migrations/000001_init.down.sql
+++ b/sql/migrations/000001_init.down.sql
@@ -1,7 +1,12 @@
+DROP TRIGGER IF EXISTS trg_update_title_rating ON usertitles;
+DROP TRIGGER IF EXISTS trg_notify_new_signal ON signals;
+
+DROP FUNCTION IF EXISTS update_title_rating();
+DROP FUNCTION IF EXISTS notify_new_signal();
+
DROP TABLE IF EXISTS signals;
DROP TABLE IF EXISTS title_tags;
DROP TABLE IF EXISTS usertitles;
-DROP TABLE IF EXISTS reviews;
DROP TABLE IF EXISTS titles;
DROP TABLE IF EXISTS studios;
DROP TABLE IF EXISTS users;
diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql
index 93ce071..0b7fa33 100644
--- a/sql/migrations/000001_init.up.sql
+++ b/sql/migrations/000001_init.up.sql
@@ -1,99 +1,141 @@
-- TODO:
--- title table triggers
-- maybe jsonb constraints
--- actions (delete)
+-- clean unused images
CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress');
CREATE TYPE storage_type_t AS ENUM ('local', 's3');
CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned');
CREATE TYPE release_season_t AS ENUM ('winter', 'spring', 'summer', 'fall');
CREATE TABLE providers (
- provider_id serial PRIMARY KEY,
- provider_name varchar(64) NOT NULL
- -- token
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ provider_name text NOT NULL,
+ credentials jsonb
);
CREATE TABLE tags (
- tag_id serial PRIMARY KEY,
- tag_names jsonb NOT NULL --mb constraints
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ tag_names jsonb NOT NULL
);
-
--- clean unused images
CREATE TABLE images (
- image_id serial PRIMARY KEY,
- storage_type storage_type_t NOT NULL,
- image_path varchar(256) UNIQUE NOT NULL
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ storage_type storage_type_t NOT NULL,
+ image_path text UNIQUE NOT NULL
);
CREATE TABLE users (
- user_id serial PRIMARY KEY,
- avatar_id int REFERENCES images (image_id),
- passhash text NOT NULL,
- mail varchar(64) CHECK (mail ~ '[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+'),
- nickname varchar(16) NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]+$'),
- disp_name varchar(32),
- user_desc varchar(512),
- -- timestamp tl dr, also add access ts
- creation_date timestamp NOT NULL
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ 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_-]+$'),
+ disp_name text,
+ user_desc text,
+ creation_date timestamptz NOT NULL,
+ last_login timestamptz
);
CREATE TABLE studios (
- studio_id serial PRIMARY KEY,
- studio_name varchar(64) UNIQUE,
- illust_id int REFERENCES images (image_id),
- studio_desc text
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ studio_name text UNIQUE,
+ illust_id bigint REFERENCES images (id),
+ studio_desc text
);
CREATE TABLE titles (
- title_id serial PRIMARY KEY,
- title_names jsonb NOT NULL,
- studio_id int NOT NULL REFERENCES studios,
- poster_id int REFERENCES images (image_id),
- --signal_ids int[] NOT NULL,
- title_status title_status_t NOT NULL,
- rating float CHECK (rating > 0 AND rating <= 10), --by trigger
- rating_count int CHECK (rating_count >= 0), --by trigger
- release_year int CHECK (release_year >= 1900),
- release_season release_season_t,
- season int CHECK (season >= 0),
- episodes_aired int CHECK (episodes_aired >= 0),
- episodes_all int CHECK (episodes_all >= 0),
- episodes_len jsonb,
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ title_names jsonb NOT NULL,
+ studio_id bigint NOT NULL REFERENCES studios (id),
+ poster_id bigint REFERENCES images (id),
+ title_status title_status_t NOT NULL,
+ rating float CHECK (rating >= 0 AND rating <= 10),
+ rating_count int CHECK (rating_count >= 0),
+ release_year int CHECK (release_year >= 1900),
+ release_season release_season_t,
+ season int CHECK (season >= 0),
+ episodes_aired int CHECK (episodes_aired >= 0),
+ episodes_all int CHECK (episodes_all >= 0),
+ episodes_len jsonb,
CHECK ((episodes_aired IS NULL AND episodes_all IS NULL)
OR (episodes_aired IS NOT NULL AND episodes_all IS NOT NULL
AND episodes_aired <= episodes_all))
);
-CREATE TABLE reviews (
- review_id serial PRIMARY KEY, --???
- user_id int NOT NULL REFERENCES users,
- title_id int NOT NULL REFERENCES titles,
- --image_ids int[], move somewhere
- review_text text NOT NULL,
- creation_date timestamp NOT NULL
- -- constrai (title, user)
-);
-
CREATE TABLE usertitles (
- usertitle_id serial PRIMARY KEY, -- bigserial, replace by (,)
- user_id int NOT NULL REFERENCES users,
- title_id int NOT NULL REFERENCES titles,
- status usertitle_status_t NOT NULL,
- rate int CHECK (rate > 0 AND rate <= 10),
- review_id int REFERENCES reviews
+ PRIMARY KEY (user_id, title_id),
+ user_id bigint NOT NULL REFERENCES users (id),
+ title_id bigint NOT NULL REFERENCES titles (id),
+ status usertitle_status_t NOT NULL,
+ rate int CHECK (rate > 0 AND rate <= 10),
+ review_text text,
+ review_date timestamptz
);
CREATE TABLE title_tags (
- PRIMARY KEY (title_id, tag_id),
- title_id int NOT NULL REFERENCES titles,
- tag_id int NOT NULL REFERENCES tags
+ PRIMARY KEY (title_id, tag_id),
+ title_id bigint NOT NULL REFERENCES titles (id),
+ tag_id bigint NOT NULL REFERENCES tags (id)
);
CREATE TABLE signals (
- signal_id serial PRIMARY KEY,
- -- title_id
- raw_data jsonb NOT NULL,
- provider_id int NOT NULL REFERENCES providers,
- dirty bool NOT NULL
-);
\ No newline at end of file
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ title_id bigint REFERENCES titles (id),
+ raw_data jsonb NOT NULL,
+ provider_id bigint NOT NULL REFERENCES providers (id),
+ pending boolean NOT NULL
+);
+
+-- Functions
+CREATE OR REPLACE FUNCTION update_title_rating()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE' AND NEW.rate IS DISTINCT FROM OLD.rate) THEN
+ UPDATE titles
+ SET
+ rating = sub.avg_rating,
+ rating_count = sub.rating_count
+ FROM (
+ SELECT
+ title_id,
+ AVG(rate)::float AS avg_rating,
+ COUNT(rate) AS rating_count
+ FROM usertitles
+ WHERE title_id = NEW.title_id AND rate IS NOT NULL
+ GROUP BY title_id
+ ) AS sub
+ WHERE titles.id = sub.title_id;
+ END IF;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION notify_new_signal()
+RETURNS TRIGGER AS $$
+DECLARE
+ payload JSON;
+BEGIN
+ payload := json_build_object(
+ 'signal_id', NEW.id,
+ 'title_id', NEW.title_id,
+ 'provider_id', NEW.provider_id,
+ 'pending', NEW.pending,
+ 'timestamp', NOW()
+ );
+ PERFORM pg_notify('new_signal', payload::text);
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Triggers
+
+CREATE TRIGGER trg_update_title_rating
+AFTER INSERT OR UPDATE OF rate ON usertitles
+FOR EACH ROW
+EXECUTE FUNCTION update_title_rating();
+
+CREATE TRIGGER trg_notify_new_signal
+AFTER INSERT ON signals
+FOR EACH ROW
+EXECUTE FUNCTION notify_new_signal();
\ No newline at end of file
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