Compare commits

...

4 commits

Author SHA1 Message Date
6a5994e33e feat: handler for get /users is implemented
All checks were successful
Build and Deploy Go App / build (push) Successful in 7m0s
Build and Deploy Go App / deploy (push) Successful in 44s
2025-12-05 20:15:05 +03:00
fe18c0d865 feat /users path is specified 2025-12-05 20:14:08 +03:00
40e341c05a feat: query SearchUsers was written 2025-12-05 20:13:16 +03:00
169bb482ce feat: desc field for title was added 2025-12-05 19:42:25 +03:00
9 changed files with 380 additions and 1 deletions

View file

@ -122,6 +122,53 @@ paths:
description: Unknown server error
security:
- JwtAuthCookies: []
/users/:
get:
summary: 'Search user by nickname or dispname (both in one param), response is always sorted by id'
parameters:
- name: word
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
format: int32
default: 10
- name: cursor_id
in: query
description: pass cursor naked
schema:
type: integer
format: int32
default: 1
responses:
'200':
description: List of users with cursor
content:
application/json:
schema:
type: object
properties:
data:
description: List of users
type: array
items:
$ref: '#/components/schemas/User'
cursor:
type: integer
format: int64
default: 1
required:
- data
- cursor
'204':
description: No users found
'400':
description: Request params are not correct
'500':
description: Unknown server error
'/users/{user_id}':
get:
operationId: getUsersId

View file

@ -201,6 +201,15 @@ type GetTitleParams struct {
Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
}
// GetUsersParams defines parameters for GetUsers.
type GetUsersParams struct {
Word *string `form:"word,omitempty" json:"word,omitempty"`
Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"`
// CursorId pass cursor naked
CursorId *int32 `form:"cursor_id,omitempty" json:"cursor_id,omitempty"`
}
// GetUsersIdParams defines parameters for GetUsersId.
type GetUsersIdParams struct {
Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
@ -276,6 +285,9 @@ type ServerInterface interface {
// Get title description
// (GET /titles/{title_id})
GetTitle(c *gin.Context, titleId int64, params GetTitleParams)
// Search user by nickname or dispname (both in one param), response is always sorted by id
// (GET /users/)
GetUsers(c *gin.Context, params GetUsersParams)
// Get user info
// (GET /users/{user_id})
GetUsersId(c *gin.Context, userId string, params GetUsersIdParams)
@ -459,6 +471,48 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) {
siw.Handler.GetTitle(c, titleId, params)
}
// GetUsers operation middleware
func (siw *ServerInterfaceWrapper) GetUsers(c *gin.Context) {
var err error
// Parameter object where we will unmarshal all parameters from the context
var params GetUsersParams
// ------------- Optional query parameter "word" -------------
err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), &params.Word)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter word: %w", err), http.StatusBadRequest)
return
}
// ------------- Optional query parameter "limit" -------------
err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), &params.Limit)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest)
return
}
// ------------- Optional query parameter "cursor_id" -------------
err = runtime.BindQueryParameter("form", true, false, "cursor_id", c.Request.URL.Query(), &params.CursorId)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter cursor_id: %w", err), http.StatusBadRequest)
return
}
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.GetUsers(c, params)
}
// GetUsersId operation middleware
func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) {
@ -799,6 +853,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options
router.GET(options.BaseURL+"/titles", wrapper.GetTitles)
router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitle)
router.GET(options.BaseURL+"/users/", wrapper.GetUsers)
router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersId)
router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser)
router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUserTitles)
@ -904,6 +959,52 @@ func (response GetTitle500Response) VisitGetTitleResponse(w http.ResponseWriter)
return nil
}
type GetUsersRequestObject struct {
Params GetUsersParams
}
type GetUsersResponseObject interface {
VisitGetUsersResponse(w http.ResponseWriter) error
}
type GetUsers200JSONResponse struct {
Cursor int64 `json:"cursor"`
// Data List of users
Data []User `json:"data"`
}
func (response GetUsers200JSONResponse) VisitGetUsersResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetUsers204Response struct {
}
func (response GetUsers204Response) VisitGetUsersResponse(w http.ResponseWriter) error {
w.WriteHeader(204)
return nil
}
type GetUsers400Response struct {
}
func (response GetUsers400Response) VisitGetUsersResponse(w http.ResponseWriter) error {
w.WriteHeader(400)
return nil
}
type GetUsers500Response struct {
}
func (response GetUsers500Response) VisitGetUsersResponse(w http.ResponseWriter) error {
w.WriteHeader(500)
return nil
}
type GetUsersIdRequestObject struct {
UserId string `json:"user_id"`
Params GetUsersIdParams
@ -1305,6 +1406,9 @@ type StrictServerInterface interface {
// Get title description
// (GET /titles/{title_id})
GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error)
// Search user by nickname or dispname (both in one param), response is always sorted by id
// (GET /users/)
GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error)
// Get user info
// (GET /users/{user_id})
GetUsersId(ctx context.Context, request GetUsersIdRequestObject) (GetUsersIdResponseObject, error)
@ -1395,6 +1499,33 @@ func (sh *strictHandler) GetTitle(ctx *gin.Context, titleId int64, params GetTit
}
}
// GetUsers operation middleware
func (sh *strictHandler) GetUsers(ctx *gin.Context, params GetUsersParams) {
var request GetUsersRequestObject
request.Params = params
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.GetUsers(ctx, request.(GetUsersRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetUsers")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(GetUsersResponseObject); ok {
if err := validResponse.VisitGetUsersResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}
// GetUsersId operation middleware
func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetUsersIdParams) {
var request GetUsersIdRequestObject

View file

@ -11,6 +11,8 @@ paths:
$ref: "./paths/titles.yaml"
/titles/{title_id}:
$ref: "./paths/titles-id.yaml"
/users/:
$ref: "./paths/users.yaml"
/users/{user_id}:
$ref: "./paths/users-id.yaml"
/users/{user_id}/titles:

46
api/paths/users.yaml Normal file
View file

@ -0,0 +1,46 @@
get:
summary: Search user by nickname or dispname (both in one param), response is always sorted by id
parameters:
- in: query
name: word
schema:
type: string
- in: query
name: limit
schema:
type: integer
format: int32
default: 10
- in: query
name: cursor_id
description: pass cursor naked
schema:
type: integer
format: int32
default: 1
responses:
'200':
description: List of users with cursor
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '../schemas/User.yaml'
description: List of users
cursor:
type: integer
format: int64
default: 1
required:
- data
- cursor
'204':
description: No users found
'400':
description: Request params are not correct
'500':
description: Unknown server error

View file

@ -485,3 +485,39 @@ func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleReque
return oapi.GetUserTitle200JSONResponse(oapi_usertitle), nil
}
// GetUsers implements oapi.StrictServerInterface.
func (s *Server) GetUsers(ctx context.Context, request oapi.GetUsersRequestObject) (oapi.GetUsersResponseObject, error) {
params := sqlc.SearchUserParams{
Word: request.Params.Word,
Cursor: request.Params.CursorId,
Limit: request.Params.Limit,
}
_users, err := s.db.SearchUser(ctx, params)
if err != nil {
log.Errorf("%v", err)
return oapi.GetUsers500Response{}, nil
}
if len(_users) == 0 {
return oapi.GetUsers204Response{}, nil
}
var users []oapi.User
var cursor int64
for _, user := range _users {
oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit
// add image
CreationDate: &user.CreationDate,
DispName: user.DispName,
Id: &user.ID,
Mail: StringToEmail(user.Mail),
Nickname: user.Nickname,
UserDesc: user.UserDesc,
}
users = append(users, oapi_user)
cursor = user.ID
}
return oapi.GetUsers200JSONResponse{Data: users, Cursor: cursor}, nil
}

View file

@ -23,6 +23,37 @@ FROM users as t
LEFT JOIN images as i ON (t.avatar_id = i.id)
WHERE t.id = sqlc.arg('id')::bigint;
-- name: SearchUser :many
SELECT
u.id AS id,
u.avatar_id AS avatar_id,
u.mail AS mail,
u.nickname AS nickname,
u.disp_name AS disp_name,
u.user_desc AS user_desc,
u.creation_date AS creation_date,
i.storage_type AS storage_type,
i.image_path AS image_path
FROM users AS u
LEFT JOIN images AS i ON u.avatar_id = i.id
WHERE
(
sqlc.narg('word')::text IS NULL
OR (
SELECT bool_and(
u.nickname ILIKE ('%' || term || '%')
OR u.disp_name ILIKE ('%' || term || '%')
)
FROM unnest(string_to_array(trim(sqlc.narg('word')::text), ' ')) AS term
WHERE term <> ''
)
)
AND (
sqlc.narg('cursor')::int IS NULL
OR u.id > sqlc.narg('cursor')::int
)
ORDER BY u.id ASC
LIMIT COALESCE(sqlc.narg('limit')::int, 20);
-- name: GetStudioByID :one
SELECT *

View file

@ -47,6 +47,8 @@ CREATE TABLE titles (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]}
title_names jsonb NOT NULL,
-- example {"ru": "Кулинарное аниме как правильно приготовить людей.","en": "A culinary anime about how to cook people properly."}
title_desc jsonb,
studio_id bigint NOT NULL REFERENCES studios (id),
poster_id bigint REFERENCES images (id) ON DELETE SET NULL,
title_status title_status_t NOT NULL,

View file

@ -246,6 +246,7 @@ type Tag struct {
type Title struct {
ID int64 `json:"id"`
TitleNames json.RawMessage `json:"title_names"`
TitleDesc []byte `json:"title_desc"`
StudioID int64 `json:"studio_id"`
PosterID *int64 `json:"poster_id"`
TitleStatus TitleStatusT `json:"title_status"`

View file

@ -129,7 +129,7 @@ func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, er
const getTitleByID = `-- name: GetTitleByID :one
SELECT
t.id, t.title_names, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len,
t.id, t.title_names, t.title_desc, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len,
i.storage_type as title_storage_type,
i.image_path as title_image_path,
COALESCE(
@ -157,6 +157,7 @@ GROUP BY
type GetTitleByIDRow struct {
ID int64 `json:"id"`
TitleNames json.RawMessage `json:"title_names"`
TitleDesc []byte `json:"title_desc"`
StudioID int64 `json:"studio_id"`
PosterID *int64 `json:"poster_id"`
TitleStatus TitleStatusT `json:"title_status"`
@ -185,6 +186,7 @@ func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByID
err := row.Scan(
&i.ID,
&i.TitleNames,
&i.TitleDesc,
&i.StudioID,
&i.PosterID,
&i.TitleStatus,
@ -638,6 +640,87 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S
return items, nil
}
const searchUser = `-- name: SearchUser :many
SELECT
u.id AS id,
u.avatar_id AS avatar_id,
u.mail AS mail,
u.nickname AS nickname,
u.disp_name AS disp_name,
u.user_desc AS user_desc,
u.creation_date AS creation_date,
i.storage_type AS storage_type,
i.image_path AS image_path
FROM users AS u
LEFT JOIN images AS i ON u.avatar_id = i.id
WHERE
(
$1::text IS NULL
OR (
SELECT bool_and(
u.nickname ILIKE ('%' || term || '%')
OR u.disp_name ILIKE ('%' || term || '%')
)
FROM unnest(string_to_array(trim($1::text), ' ')) AS term
WHERE term <> ''
)
)
AND (
$2::int IS NULL
OR u.id > $2::int
)
ORDER BY u.id ASC
LIMIT COALESCE($3::int, 20)
`
type SearchUserParams struct {
Word *string `json:"word"`
Cursor *int32 `json:"cursor"`
Limit *int32 `json:"limit"`
}
type SearchUserRow 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"`
StorageType *StorageTypeT `json:"storage_type"`
ImagePath *string `json:"image_path"`
}
func (q *Queries) SearchUser(ctx context.Context, arg SearchUserParams) ([]SearchUserRow, error) {
rows, err := q.db.Query(ctx, searchUser, arg.Word, arg.Cursor, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchUserRow{}
for rows.Next() {
var i SearchUserRow
if err := rows.Scan(
&i.ID,
&i.AvatarID,
&i.Mail,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
&i.StorageType,
&i.ImagePath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchUserTitles = `-- name: SearchUserTitles :many
SELECT