From ba68b5ee0465fd70e15d229591df9d8dfaacba64 Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Sat, 22 Nov 2025 07:09:30 +0300 Subject: [PATCH 1/5] feat: getusertitles now must return all the title info --- api/_build/openapi.yaml | 5 ++--- api/api.gen.go | 18 ++++++++++-------- api/schemas/UserTitle.yaml | 5 ++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index c059166..c07dbbc 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -424,9 +424,8 @@ components: user_id: type: integer format: int64 - title_id: - type: integer - format: int64 + title: + $ref: '#/components/schemas/Title' status: $ref: '#/components/schemas/UserTitleStatus' rate: diff --git a/api/api.gen.go b/api/api.gen.go index e56f6b8..320e7a2 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -143,7 +143,7 @@ type UserTitle struct { // Status User's title status Status UserTitleStatus `json:"status"` - TitleId int64 `json:"title_id"` + Title *Title `json:"title,omitempty"` UserId int64 `json:"user_id"` AdditionalProperties map[string]interface{} `json:"-"` } @@ -493,12 +493,12 @@ func (a *UserTitle) UnmarshalJSON(b []byte) error { delete(object, "status") } - if raw, found := object["title_id"]; found { - err = json.Unmarshal(raw, &a.TitleId) + if raw, found := object["title"]; found { + err = json.Unmarshal(raw, &a.Title) if err != nil { - return fmt.Errorf("error reading 'title_id': %w", err) + return fmt.Errorf("error reading 'title': %w", err) } - delete(object, "title_id") + delete(object, "title") } if raw, found := object["user_id"]; found { @@ -554,9 +554,11 @@ func (a UserTitle) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("error marshaling 'status': %w", err) } - object["title_id"], err = json.Marshal(a.TitleId) - if err != nil { - return nil, fmt.Errorf("error marshaling 'title_id': %w", err) + if a.Title != nil { + object["title"], err = json.Marshal(a.Title) + if err != nil { + return nil, fmt.Errorf("error marshaling 'title': %w", err) + } } object["user_id"], err = json.Marshal(a.UserId) diff --git a/api/schemas/UserTitle.yaml b/api/schemas/UserTitle.yaml index 658e350..3beaec6 100644 --- a/api/schemas/UserTitle.yaml +++ b/api/schemas/UserTitle.yaml @@ -7,9 +7,8 @@ properties: user_id: type: integer format: int64 - title_id: - type: integer - format: int64 + title: + $ref: ./Title.yaml status: $ref: ./enums/UserTitleStatus.yaml rate: From 32566fe7a2cc5d2469f9f47c1dbd55f35859d18f Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Sat, 22 Nov 2025 07:53:50 +0300 Subject: [PATCH 2/5] feat: query for usertitles written --- api/_build/openapi.yaml | 7 +- api/api.gen.go | 10 +- api/paths/users-id-titles.yaml | 7 +- modules/backend/handlers/common.go | 117 +++++++------- modules/backend/handlers/titles.go | 72 --------- modules/backend/handlers/users.go | 138 +++++++++++++--- modules/backend/queries.sql | 144 +++++++++++++---- sql/queries.sql.go | 252 +++++++++++++++++++++-------- 8 files changed, 497 insertions(+), 250 deletions(-) diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index c07dbbc..722b7af 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -158,7 +158,12 @@ paths: - in: query name: status schema: - $ref: '#/components/schemas/TitleStatus' + type: array + items: + $ref: '#/components/schemas/TitleStatus' + description: List of title statuses to filter + style: form + explode: false - in: query name: watch_status schema: diff --git a/api/api.gen.go b/api/api.gen.go index 320e7a2..54c8fc1 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -183,9 +183,11 @@ type GetUsersUserIdParams struct { // GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles. type GetUsersUserIdTitlesParams struct { - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - Word *string `form:"word,omitempty" json:"word,omitempty"` - Status *TitleStatus `form:"status,omitempty" json:"status,omitempty"` + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` + Word *string `form:"word,omitempty" json:"word,omitempty"` + + // Status List of title statuses to filter + Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"` WatchStatus *UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"` Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` @@ -811,7 +813,7 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { // ------------- Optional query parameter "status" ------------- - err = runtime.BindQueryParameter("form", true, false, "status", c.Request.URL.Query(), ¶ms.Status) + err = runtime.BindQueryParameter("form", false, false, "status", c.Request.URL.Query(), ¶ms.Status) if err != nil { siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest) return diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 0cde5af..0788319 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -14,7 +14,12 @@ get: - in: query name: status schema: - $ref: '../schemas/enums/TitleStatus.yaml' + type: array + items: + $ref: '../schemas/enums/TitleStatus.yaml' + description: List of title statuses to filter + style: form + explode: false - in: query name: watch_status schema: diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 3d61b91..6618d49 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -1,6 +1,10 @@ package handlers import ( + "context" + "encoding/json" + "fmt" + oapi "nyanimedb/api" sqlc "nyanimedb/sql" "strconv" ) @@ -13,70 +17,75 @@ func NewServer(db *sqlc.Queries) Server { return Server{db: db} } -// type Cursor interface { -// ParseCursor(sortBy oapi.TitleSort, data oapi.Cursor) (Cursor, error) +func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi.Title, error) { -// Values() map[string]interface{} -// // for logs only -// Type() string -// } + title_names := make(map[string][]string, 0) + err := json.Unmarshal(title.TitleNames, &title_names) + if err != nil { + return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) + } -// type CursorByID struct { -// ID int64 -// } + episodes_lens := make(map[string]float64, 0) + err = json.Unmarshal(title.EpisodesLen, &episodes_lens) + if err != nil { + return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) + } -// func (c CursorByID) ParseCursor(sortBy oapi.TitleSort, data oapi.Cursor) (Cursor, error) { -// var cur CursorByID -// if err := json.Unmarshal(data, &cur); err != nil { -// return nil, fmt.Errorf("invalid cursor (id): %w", err) -// } -// if cur.ID == 0 { -// return nil, errors.New("cursor id must be non-zero") -// } -// return cur, nil -// } + oapi_tag_names := make(oapi.Tags, 0) + err = json.Unmarshal(title.TagNames, &oapi_tag_names) + if err != nil { + return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) + } -// func (c CursorByID) Values() map[string]interface{} { -// return map[string]interface{}{ -// "cursor_id": c.ID, -// "cursor_year": nil, -// "cursor_rating": nil, -// } -// } + var oapi_studio oapi.Studio -// func (c CursorByID) Type() string { return "id" } + oapi_studio.Id = title.StudioID + if title.StudioName != nil { + oapi_studio.Name = *title.StudioName + } + oapi_studio.Description = title.StudioDesc + if title.StudioIllustID != nil { + oapi_studio.Poster.Id = title.StudioIllustID + oapi_studio.Poster.ImagePath = title.StudioImagePath + oapi_studio.Poster.StorageType = &title.StudioStorageType + } -// func NewCursor(sortBy string) (Cursor, error) { -// switch Type(sortBy) { -// case TypeID: -// return CursorByID{}, nil -// case TypeYear: -// return CursorByYear{}, nil -// case TypeRating: -// return CursorByRating{}, nil -// default: -// return nil, fmt.Errorf("unsupported sort_by: %q", sortBy) -// } -// } + var oapi_image oapi.Image -// decodes a base64-encoded JSON string into a CursorObj -// Returns the parsed CursorObj and an error -// func parseCursor(encoded oapi.Cursor) (*oapi.CursorObj, error) { + if title.PosterID != nil { + oapi_image.Id = title.PosterID + oapi_image.ImagePath = title.TitleImagePath + oapi_image.StorageType = &title.TitleStorageType + } -// // Decode base64 -// decoded, err := base64.StdEncoding.DecodeString(encoded) -// if err != nil { -// return nil, fmt.Errorf("parseCursor: %v", err) -// } + var release_season oapi.ReleaseSeason + if title.ReleaseSeason != nil { + release_season = oapi.ReleaseSeason(*title.ReleaseSeason) + } -// // Parse JSON -// var cursor oapi.CursorObj -// if err := json.Unmarshal(decoded, &cursor); err != nil { -// return nil, fmt.Errorf("parseCursor: %v", err) -// } + oapi_status, err := TitleStatus2oapi(&title.TitleStatus) + if err != nil { + return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) + } + oapi_title := oapi.Title{ + EpisodesAired: title.EpisodesAired, + EpisodesAll: title.EpisodesAired, + EpisodesLen: &episodes_lens, + Id: title.ID, + Poster: &oapi_image, + Rating: title.Rating, + RatingCount: title.RatingCount, + ReleaseSeason: &release_season, + ReleaseYear: title.ReleaseYear, + Studio: &oapi_studio, + Tags: oapi_tag_names, + TitleNames: title_names, + TitleStatus: oapi_status, + // AdditionalProperties: + } -// return &cursor, nil -// } + return oapi_title, nil +} func parseInt64(s string) (int32, error) { i, err := strconv.ParseInt(s, 10, 64) diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index 84fc87e..332a4b6 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -158,78 +158,6 @@ func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) { return &oapi_studio, nil } -func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi.Title, error) { - - // var oapi_title oapi.Title - - title_names := make(map[string][]string, 0) - err := json.Unmarshal(title.TitleNames, &title_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) - } - - episodes_lens := make(map[string]float64, 0) - err = json.Unmarshal(title.EpisodesLen, &episodes_lens) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) - } - - oapi_tag_names := make(oapi.Tags, 0) - err = json.Unmarshal(title.TagNames, &oapi_tag_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) - } - - var oapi_studio oapi.Studio - - oapi_studio.Id = title.StudioID - if title.StudioName != nil { - oapi_studio.Name = *title.StudioName - } - oapi_studio.Description = title.StudioDesc - if title.StudioIllustID != nil { - oapi_studio.Poster.Id = title.StudioIllustID - oapi_studio.Poster.ImagePath = title.StudioImagePath - oapi_studio.Poster.StorageType = &title.StudioStorageType - } - - var oapi_image oapi.Image - - if title.PosterID != nil { - oapi_image.Id = title.PosterID - oapi_image.ImagePath = title.TitleImagePath - oapi_image.StorageType = &title.TitleStorageType - } - - var release_season oapi.ReleaseSeason - if title.ReleaseSeason != nil { - release_season = oapi.ReleaseSeason(*title.ReleaseSeason) - } - - oapi_status, err := TitleStatus2oapi(&title.TitleStatus) - if err != nil { - return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) - } - oapi_title := oapi.Title{ - EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAired, - EpisodesLen: &episodes_lens, - Id: title.ID, - Poster: &oapi_image, - Rating: title.Rating, - RatingCount: title.RatingCount, - ReleaseSeason: &release_season, - ReleaseYear: title.ReleaseYear, - Studio: &oapi_studio, - Tags: oapi_tag_names, - TitleNames: title_names, - TitleStatus: oapi_status, - // AdditionalProperties: - } - - return oapi_title, nil -} - func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitleIdRequestObject) (oapi.GetTitlesTitleIdResponseObject, error) { var oapi_title oapi.Title diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index 0fa903f..0420b91 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -2,12 +2,15 @@ package handlers import ( "context" + "fmt" oapi "nyanimedb/api" sqlc "nyanimedb/sql" "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" "github.com/oapi-codegen/runtime/types" + log "github.com/sirupsen/logrus" ) // type Server struct { @@ -50,33 +53,120 @@ func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdReque return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil } +func sqlDate2oapi(p_date pgtype.Timestamptz) (time.Time, error) { + return time.Time{}, nil +} + +type SqlcUserStatus struct { + dropped string + finished string + planned string + in_progress string +} + +func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { + var sqlc_status SqlcUserStatus + if s == nil { + return &sqlc_status, nil + } + for _, t := range *s { + switch t { + case oapi.UserTitleStatusFinished: + sqlc_status.finished = "finished" + case oapi.UserTitleStatusDropped: + sqlc_status.dropped = "dropped" + case oapi.UserTitleStatusPlanned: + sqlc_status.planned = "planned" + case oapi.UserTitleStatusInProgress: + sqlc_status.in_progress = "in-progress" + default: + return nil, fmt.Errorf("unexpected tittle status: %s", t) + } + } + return &sqlc_status, nil +} + func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersUserIdTitlesRequestObject) (oapi.GetUsersUserIdTitlesResponseObject, error) { - var rate int32 = 9 - var review_id int64 = 3 - time := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC) + oapi_usertitles := make([]oapi.UserTitle, 0) - var userTitles = []oapi.UserTitle{ - { - UserId: 101, - TitleId: 2001, - Status: oapi.UserTitleStatusFinished, - Rate: &rate, - Ctime: &time, - }, - { - UserId: 102, - TitleId: 2002, - Status: oapi.UserTitleStatusInProgress, - ReviewId: &review_id, - Ctime: &time, - }, - { - UserId: 103, - TitleId: 2003, - Status: oapi.UserTitleStatusDropped, - Ctime: &time, - }, + word := Word2Sqlc(request.Params.Word) + status, err := TitleStatus2Sqlc(request.Params.Status) + if err != nil { + log.Errorf("%v", err) + return oapi.GetUsersUserIdTitles400Response{}, err + } + + season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) + if err != nil { + log.Errorf("%v", err) + return oapi.GetUsersUserIdTitles400Response{}, err + } + + // Forward bool `json:"forward"` + // SortBy string `json:"sort_by"` + // CursorYear *int32 `json:"cursor_year"` + // CursorID *int64 `json:"cursor_id"` + // CursorRating *float64 `json:"cursor_rating"` + // Word *string `json:"word"` + // Ongoing string `json:"ongoing"` + // Planned string `json:"planned"` + // Dropped string `json:"dropped"` + // InProgress string `json:"in-progress"` + // Finished string `json:"finished"` + // Rate *int32 `json:"rate"` + // Rating *float64 `json:"rating"` + // ReleaseYear *int32 `json:"release_year"` + // ReleaseSeason *ReleaseSeasonT `json:"release_season"` + // Limit *int32 `json:"limit"` + params := sqlc.SearchTitlesParams{ + Word: word, + Ongoing: status.ongoing, + Finished: status.finished, + Planned: status.planned, + Rating: request.Params.Rating, + ReleaseYear: request.Params.ReleaseYear, + ReleaseSeason: season, + Forward: true, + SortBy: "id", + Limit: request.Params.Limit, + } + + if request.Params.SortForward != nil { + params.Forward = *request.Params.SortForward + } + if request.Params.Sort != nil { + params.SortBy = string(*request.Params.Sort) + if request.Params.Cursor != nil { + err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) + if err != nil { + log.Errorf("%v", err) + return oapi.GetTitles400Response{}, nil + } + } + } + + _sqlc_title := sqlc.SearchTitlesRow{ + ID: sqlc_title.ID, + StudioID: sqlc_title.StudioID, + PosterID: sqlc_title.PosterID, + TitleStatus: sqlc_title.TitleStatus, + Rating: sqlc_title.Rating, + RatingCount: sqlc_title.RatingCount, + ReleaseYear: sqlc_title.ReleaseYear, + ReleaseSeason: sqlc_title.ReleaseSeason, + Season: sqlc_title.Season, + EpisodesAired: sqlc_title.EpisodesAired, + EpisodesAll: sqlc_title.EpisodesAll, + EpisodesLen: sqlc_title.EpisodesLen, + TitleStorageType: sqlc_title.TitleStorageType, + TitleImagePath: sqlc_title.TitleImagePath, + TagNames: sqlc_title.TitleNames, + StudioName: sqlc_title.StudioName, + StudioIllustID: sqlc_title.StudioIllustID, + StudioDesc: sqlc_title.StudioDesc, + StudioStorageType: sqlc_title.StudioStorageType, + StudioImagePath: sqlc_title.StudioImagePath, } return oapi.GetUsersUserIdTitles200JSONResponse(userTitles), nil diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 16f8120..b8cb8aa 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -86,7 +86,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -109,7 +109,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -210,37 +210,125 @@ ORDER BY LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit -- name: SearchUserTitles :many -SELECT - * -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -WHERE - CASE - WHEN sqlc.narg('word')::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true +SELECT + t.*, + u.user_id as user_id, + u.status as usertitle_status, + u.rate as user_rate, + u.review_id as review_id, + u.ctime as user_ctime, + i.storage_type::text as title_storage_type, + i.image_path as title_image_path, + jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + s.studio_name as studio_name, + s.illust_id as studio_illust_id, + s.studio_desc as studio_desc, + si.storage_type::text as studio_storage_type, + si.image_path as studio_image_path + +FROM usertitles as u +LEFT JOIN titles as t ON (u.title_id = t.id) +LEFT JOIN images as i ON (t.poster_id = i.id) +LEFT JOIN title_tags as tt ON (t.id = tt.title_id) +LEFT JOIN tags as g ON (tt.tag_id = g.id) +LEFT JOIN studios as s ON (t.studio_id = s.id) +LEFT JOIN images as si ON (s.illust_id = si.id) + +WHERE + CASE + WHEN sqlc.arg('forward')::boolean THEN + -- forward: greater than cursor (next page) + CASE sqlc.arg('sort_by')::text + WHEN 'year' THEN + (sqlc.narg('cursor_year')::int IS NULL) OR + (t.release_year > sqlc.narg('cursor_year')::int) OR + (t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint) + + WHEN 'rating' THEN + (sqlc.narg('cursor_rating')::float IS NULL) OR + (t.rating > sqlc.narg('cursor_rating')::float) OR + (t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint) + + WHEN 'id' THEN + (sqlc.narg('cursor_id')::bigint IS NULL) OR + (t.id > sqlc.narg('cursor_id')::bigint) + + ELSE true -- fallback + END + + ELSE + -- backward: less than cursor (prev page) + CASE sqlc.arg('sort_by')::text + WHEN 'year' THEN + (sqlc.narg('cursor_year')::int IS NULL) OR + (t.release_year < sqlc.narg('cursor_year')::int) OR + (t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint) + + WHEN 'rating' THEN + (sqlc.narg('cursor_rating')::float IS NULL) OR + (t.rating < sqlc.narg('cursor_rating')::float) OR + (t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint) + + WHEN 'id' THEN + (sqlc.narg('cursor_id')::bigint IS NULL) OR + (t.id < sqlc.narg('cursor_id')::bigint) + + ELSE true + END END - AND (sqlc.narg('status')::title_status_t IS NULL OR t.title_status = sqlc.narg('status')::title_status_t) + AND ( + CASE + WHEN sqlc.narg('word')::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(t.title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true + END + ) + + AND (u.status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('planned')::text, sqlc.arg('dropped')::text, sqlc.arg('in-progress')::text)) + AND (t.title_status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('finished')::text, sqlc.arg('planned')::text)) + AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) - AND (sqlc.narg('usertitle_status')::usertitle_status_t IS NULL OR u.usertitle_status = sqlc.narg('usertitle_status')::usertitle_status_t) + +GROUP BY + t.id, i.id, s.id, si.id + +ORDER BY + CASE WHEN sqlc.arg('forward')::boolean THEN + CASE + WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id + WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year + WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating + WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate + END + END ASC, + CASE WHEN NOT sqlc.arg('forward')::boolean THEN + CASE + WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id + WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year + WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating + WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate + END + END DESC, + + CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 4342a12..2a90265 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -128,7 +128,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -349,7 +349,7 @@ SELECT si.image_path as studio_image_path FROM titles as t -LEFT JOIN images as i ON (t.image_id = i.id) +LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) @@ -548,82 +548,195 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S const searchUserTitles = `-- name: SearchUserTitles :many -SELECT - user_id, title_id, status, rate, review_id, ctime, id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -WHERE - CASE - WHEN $1::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array($1::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true +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, + u.user_id as user_id, + u.status as usertitle_status, + u.rate as user_rate, + u.review_id as review_id, + u.ctime as user_ctime, + i.storage_type::text as title_storage_type, + i.image_path as title_image_path, + jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + s.studio_name as studio_name, + s.illust_id as studio_illust_id, + s.studio_desc as studio_desc, + si.storage_type::text as studio_storage_type, + si.image_path as studio_image_path + +FROM usertitles as u +LEFT JOIN titles as t ON (u.title_id = t.id) +LEFT JOIN images as i ON (t.poster_id = i.id) +LEFT JOIN title_tags as tt ON (t.id = tt.title_id) +LEFT JOIN tags as g ON (tt.tag_id = g.id) +LEFT JOIN studios as s ON (t.studio_id = s.id) +LEFT JOIN images as si ON (s.illust_id = si.id) + +WHERE + CASE + WHEN $1::boolean THEN + -- forward: greater than cursor (next page) + CASE $2::text + WHEN 'year' THEN + ($3::int IS NULL) OR + (t.release_year > $3::int) OR + (t.release_year = $3::int AND t.id > $4::bigint) + + WHEN 'rating' THEN + ($5::float IS NULL) OR + (t.rating > $5::float) OR + (t.rating = $5::float AND t.id > $4::bigint) + + WHEN 'id' THEN + ($4::bigint IS NULL) OR + (t.id > $4::bigint) + + ELSE true -- fallback + END + + ELSE + -- backward: less than cursor (prev page) + CASE $2::text + WHEN 'year' THEN + ($3::int IS NULL) OR + (t.release_year < $3::int) OR + (t.release_year = $3::int AND t.id < $4::bigint) + + WHEN 'rating' THEN + ($5::float IS NULL) OR + (t.rating < $5::float) OR + (t.rating = $5::float AND t.id < $4::bigint) + + WHEN 'id' THEN + ($4::bigint IS NULL) OR + (t.id < $4::bigint) + + ELSE true + END END - AND ($2::title_status_t IS NULL OR t.title_status = $2::title_status_t) - AND ($3::float IS NULL OR t.rating >= $3::float) - AND ($4::int IS NULL OR t.release_year = $4::int) - AND ($5::release_season_t IS NULL OR t.release_season = $5::release_season_t) - AND ($6::usertitle_status_t IS NULL OR u.usertitle_status = $6::usertitle_status_t) + AND ( + CASE + WHEN $6::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(t.title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array($6::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true + END + ) -LIMIT COALESCE($7::int, 100) + AND (u.status::text IN ($7::text, $8::text, $9::text, $10::text)) + AND (t.title_status::text IN ($7::text, $11::text, $8::text)) + AND ($12::int IS NULL OR u.rate >= $12::int) + AND ($13::float IS NULL OR t.rating >= $13::float) + AND ($14::int IS NULL OR t.release_year = $14::int) + AND ($15::release_season_t IS NULL OR t.release_season = $15::release_season_t) + +GROUP BY + t.id, i.id, s.id, si.id + +ORDER BY + CASE WHEN $1::boolean THEN + CASE + WHEN $2::text = 'id' THEN t.id + WHEN $2::text = 'year' THEN t.release_year + WHEN $2::text = 'rating' THEN t.rating + WHEN $2::text = 'rate' THEN u.rate + END + END ASC, + CASE WHEN NOT $1::boolean THEN + CASE + WHEN $2::text = 'id' THEN t.id + WHEN $2::text = 'year' THEN t.release_year + WHEN $2::text = 'rating' THEN t.rating + WHEN $2::text = 'rate' THEN u.rate + END + END DESC, + + CASE WHEN $2::text <> 'id' THEN t.id END ASC + +LIMIT COALESCE($16::int, 100) ` type SearchUserTitlesParams struct { - Word *string `json:"word"` - Status *TitleStatusT `json:"status"` - Rating *float64 `json:"rating"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - UsertitleStatus NullUsertitleStatusT `json:"usertitle_status"` - Limit *int32 `json:"limit"` + Forward bool `json:"forward"` + SortBy string `json:"sort_by"` + CursorYear *int32 `json:"cursor_year"` + CursorID *int64 `json:"cursor_id"` + CursorRating *float64 `json:"cursor_rating"` + Word *string `json:"word"` + Ongoing string `json:"ongoing"` + Planned string `json:"planned"` + Dropped string `json:"dropped"` + InProgress string `json:"in-progress"` + Finished string `json:"finished"` + Rate *int32 `json:"rate"` + Rating *float64 `json:"rating"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Limit *int32 `json:"limit"` } type SearchUserTitlesRow struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` - Ctime pgtype.Timestamptz `json:"ctime"` - ID int64 `json:"id"` - TitleNames []byte `json:"title_names"` - StudioID int64 `json:"studio_id"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - EpisodesLen []byte `json:"episodes_len"` + ID *int64 `json:"id"` + TitleNames []byte `json:"title_names"` + StudioID *int64 `json:"studio_id"` + PosterID *int64 `json:"poster_id"` + TitleStatus *TitleStatusT `json:"title_status"` + Rating *float64 `json:"rating"` + RatingCount *int32 `json:"rating_count"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Season *int32 `json:"season"` + EpisodesAired *int32 `json:"episodes_aired"` + EpisodesAll *int32 `json:"episodes_all"` + EpisodesLen []byte `json:"episodes_len"` + UserID int64 `json:"user_id"` + UsertitleStatus UsertitleStatusT `json:"usertitle_status"` + UserRate *int32 `json:"user_rate"` + ReviewID *int64 `json:"review_id"` + UserCtime pgtype.Timestamptz `json:"user_ctime"` + TitleStorageType string `json:"title_storage_type"` + TitleImagePath *string `json:"title_image_path"` + TagNames []byte `json:"tag_names"` + StudioName *string `json:"studio_name"` + StudioIllustID *int64 `json:"studio_illust_id"` + StudioDesc *string `json:"studio_desc"` + StudioStorageType string `json:"studio_storage_type"` + StudioImagePath *string `json:"studio_image_path"` } // 100 is default limit func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) { rows, err := q.db.Query(ctx, searchUserTitles, + arg.Forward, + arg.SortBy, + arg.CursorYear, + arg.CursorID, + arg.CursorRating, arg.Word, - arg.Status, + arg.Ongoing, + arg.Planned, + arg.Dropped, + arg.InProgress, + arg.Finished, + arg.Rate, arg.Rating, arg.ReleaseYear, arg.ReleaseSeason, - arg.UsertitleStatus, arg.Limit, ) if err != nil { @@ -634,12 +747,6 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara for rows.Next() { var i SearchUserTitlesRow if err := rows.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, &i.ID, &i.TitleNames, &i.StudioID, @@ -653,6 +760,19 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara &i.EpisodesAired, &i.EpisodesAll, &i.EpisodesLen, + &i.UserID, + &i.UsertitleStatus, + &i.UserRate, + &i.ReviewID, + &i.UserCtime, + &i.TitleStorageType, + &i.TitleImagePath, + &i.TagNames, + &i.StudioName, + &i.StudioIllustID, + &i.StudioDesc, + &i.StudioStorageType, + &i.StudioImagePath, ); err != nil { return nil, err } From f1ba15d3a4e4ccaf68d8db535d03560b191ac7db Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Sat, 22 Nov 2025 08:32:13 +0300 Subject: [PATCH 3/5] fix --- modules/backend/queries.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index b8cb8aa..a16dcbc 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -78,7 +78,7 @@ SELECT t.*, i.storage_type::text as title_storage_type, i.image_path as title_image_path, - jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + jsonb_agg(g.tag_name)'[]'::jsonb as tag_names, s.studio_name as studio_name, s.illust_id as studio_illust_id, s.studio_desc as studio_desc, @@ -101,7 +101,7 @@ SELECT t.*, i.storage_type::text as title_storage_type, i.image_path as title_image_path, - jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + jsonb_agg(g.tag_name)'[]'::jsonb as tag_names, s.studio_name as studio_name, s.illust_id as studio_illust_id, s.studio_desc as studio_desc, @@ -219,7 +219,7 @@ SELECT u.ctime as user_ctime, i.storage_type::text as title_storage_type, i.image_path as title_image_path, - jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + jsonb_agg(g.tag_name)'[]'::jsonb as tag_names, s.studio_name as studio_name, s.illust_id as studio_illust_id, s.studio_desc as studio_desc, From cbbc2c179d2421f98ad9857441ec5fd0d0b6fa12 Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Sat, 22 Nov 2025 18:29:49 +0300 Subject: [PATCH 4/5] feat: --- modules/backend/handlers/users.go | 100 +++++++++++++++--------------- modules/backend/queries.sql | 10 ++- sql/queries.sql.go | 30 ++++----- 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index 0420b91..1ea2c1a 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -103,33 +103,23 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU return oapi.GetUsersUserIdTitles400Response{}, err } - // Forward bool `json:"forward"` - // SortBy string `json:"sort_by"` - // CursorYear *int32 `json:"cursor_year"` - // CursorID *int64 `json:"cursor_id"` - // CursorRating *float64 `json:"cursor_rating"` - // Word *string `json:"word"` - // Ongoing string `json:"ongoing"` - // Planned string `json:"planned"` - // Dropped string `json:"dropped"` - // InProgress string `json:"in-progress"` - // Finished string `json:"finished"` - // Rate *int32 `json:"rate"` - // Rating *float64 `json:"rating"` - // ReleaseYear *int32 `json:"release_year"` - // ReleaseSeason *ReleaseSeasonT `json:"release_season"` - // Limit *int32 `json:"limit"` - params := sqlc.SearchTitlesParams{ - Word: word, - Ongoing: status.ongoing, - Finished: status.finished, - Planned: status.planned, - Rating: request.Params.Rating, - ReleaseYear: request.Params.ReleaseYear, - ReleaseSeason: season, - Forward: true, - SortBy: "id", - Limit: request.Params.Limit, + params := sqlc.SearchUserTitlesParams{ + Forward: true, + SortBy: "id", + // CursorYear : + // CursorID *int64 `json:"cursor_id"` + // CursorRating *float64 `json:"cursor_rating"` + // Word *string `json:"word"` + // Ongoing string `json:"ongoing"` + // Planned string `json:"planned"` + // Dropped string `json:"dropped"` + // InProgress string `json:"in-progress"` + // Finished string `json:"finished"` + // Rate *int32 `json:"rate"` + // Rating *float64 `json:"rating"` + // ReleaseYear *int32 `json:"release_year"` + // ReleaseSeason *ReleaseSeasonT `json:"release_season"` + // Limit *int32 `json:"limit"` } if request.Params.SortForward != nil { @@ -141,32 +131,42 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) if err != nil { log.Errorf("%v", err) - return oapi.GetTitles400Response{}, nil + return oapi.GetUsersUserIdTitles400Response{}, nil } } } - - _sqlc_title := sqlc.SearchTitlesRow{ - ID: sqlc_title.ID, - StudioID: sqlc_title.StudioID, - PosterID: sqlc_title.PosterID, - TitleStatus: sqlc_title.TitleStatus, - Rating: sqlc_title.Rating, - RatingCount: sqlc_title.RatingCount, - ReleaseYear: sqlc_title.ReleaseYear, - ReleaseSeason: sqlc_title.ReleaseSeason, - Season: sqlc_title.Season, - EpisodesAired: sqlc_title.EpisodesAired, - EpisodesAll: sqlc_title.EpisodesAll, - EpisodesLen: sqlc_title.EpisodesLen, - TitleStorageType: sqlc_title.TitleStorageType, - TitleImagePath: sqlc_title.TitleImagePath, - TagNames: sqlc_title.TitleNames, - StudioName: sqlc_title.StudioName, - StudioIllustID: sqlc_title.StudioIllustID, - StudioDesc: sqlc_title.StudioDesc, - StudioStorageType: sqlc_title.StudioStorageType, - StudioImagePath: sqlc_title.StudioImagePath, + // param = nil means it will not be used + titles, err := s.db.SearchUserTitles(ctx, params) + if err != nil { + log.Errorf("%v", err) + return oapi.GetUsersUserIdTitles500Response{}, nil + } + if len(titles) == 0 { + return oapi.GetUsersUserIdTitles204Response{}, nil + } + for _, title := range titles { + _sqlc_title := sqlc.SearchTitlesRow{ + ID: sqlc_title.ID, + StudioID: sqlc_title.StudioID, + PosterID: sqlc_title.PosterID, + TitleStatus: sqlc_title.TitleStatus, + Rating: sqlc_title.Rating, + RatingCount: sqlc_title.RatingCount, + ReleaseYear: sqlc_title.ReleaseYear, + ReleaseSeason: sqlc_title.ReleaseSeason, + Season: sqlc_title.Season, + EpisodesAired: sqlc_title.EpisodesAired, + EpisodesAll: sqlc_title.EpisodesAll, + EpisodesLen: sqlc_title.EpisodesLen, + TitleStorageType: sqlc_title.TitleStorageType, + TitleImagePath: sqlc_title.TitleImagePath, + TagNames: sqlc_title.TitleNames, + StudioName: sqlc_title.StudioName, + StudioIllustID: sqlc_title.StudioIllustID, + StudioDesc: sqlc_title.StudioDesc, + StudioStorageType: sqlc_title.StudioStorageType, + StudioImagePath: sqlc_title.StudioImagePath, + } } return oapi.GetUsersUserIdTitles200JSONResponse(userTitles), nil diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 307eb4f..ee2a84a 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -95,7 +95,7 @@ LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) LEFT JOIN images as si ON (s.illust_id = si.id) -WHERE id = sqlc.arg('title_id')::bigint +WHERE t.id = sqlc.arg('title_id')::bigint GROUP BY t.id, i.id, s.id, si.id; @@ -187,7 +187,13 @@ WHERE END ) - AND (t.title_status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('finished')::text, sqlc.arg('planned')::text)) + AND ( + -- Если массив пуст (NULL или []) — не фильтруем + cardinality(sqlc.arg('title_statuses')::text[]) = 0 + OR + -- Иначе: статус есть в списке + t.title_status = ANY(sqlc.arg('title_statuses')::text[]) +) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) diff --git a/sql/queries.sql.go b/sql/queries.sql.go index d4fb7a6..bf2f08a 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -138,7 +138,7 @@ LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) LEFT JOIN images as si ON (s.illust_id = si.id) -WHERE id = $1::bigint +WHERE t.id = $1::bigint GROUP BY t.id, i.id, s.id, si.id ` @@ -428,10 +428,16 @@ WHERE END ) - AND (t.title_status::text IN ($7::text, $8::text, $9::text)) - AND ($10::float IS NULL OR t.rating >= $10::float) - AND ($11::int IS NULL OR t.release_year = $11::int) - AND ($12::release_season_t IS NULL OR t.release_season = $12::release_season_t) + AND ( + -- Если массив пуст (NULL или []) — не фильтруем + cardinality($7::text[]) = 0 + OR + -- Иначе: статус есть в списке + t.title_status = ANY($7::text[]) +) + AND ($8::float IS NULL OR t.rating >= $8::float) + AND ($9::int IS NULL OR t.release_year = $9::int) + AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t) GROUP BY t.id, i.id, s.id, si.id @@ -454,7 +460,7 @@ ORDER BY CASE WHEN $2::text <> 'id' THEN t.id END ASC -LIMIT COALESCE($13::int, 100) +LIMIT COALESCE($11::int, 100) ` type SearchTitlesParams struct { @@ -464,9 +470,7 @@ type SearchTitlesParams struct { CursorID *int64 `json:"cursor_id"` CursorRating *float64 `json:"cursor_rating"` Word *string `json:"word"` - Ongoing string `json:"ongoing"` - Finished string `json:"finished"` - Planned string `json:"planned"` + TitleStatuses []string `json:"title_statuses"` Rating *float64 `json:"rating"` ReleaseYear *int32 `json:"release_year"` ReleaseSeason *ReleaseSeasonT `json:"release_season"` @@ -505,9 +509,7 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S arg.CursorID, arg.CursorRating, arg.Word, - arg.Ongoing, - arg.Finished, - arg.Planned, + arg.TitleStatuses, arg.Rating, arg.ReleaseYear, arg.ReleaseSeason, @@ -564,7 +566,7 @@ SELECT u.ctime as user_ctime, i.storage_type::text as title_storage_type, i.image_path as title_image_path, - jsonb_agg_strict(g.tag_name)'[]'::jsonb as tag_names, + jsonb_agg(g.tag_name)'[]'::jsonb as tag_names, s.studio_name as studio_name, s.illust_id as studio_illust_id, s.studio_desc as studio_desc, @@ -718,7 +720,7 @@ type SearchUserTitlesRow struct { UserCtime pgtype.Timestamptz `json:"user_ctime"` TitleStorageType string `json:"title_storage_type"` TitleImagePath *string `json:"title_image_path"` - TagNames []byte `json:"tag_names"` + TagNames json.RawMessage `json:"tag_names"` StudioName *string `json:"studio_name"` StudioIllustID *int64 `json:"studio_illust_id"` StudioDesc *string `json:"studio_desc"` From e792d5780b67d210a60e71a2566848c247384042 Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Mon, 24 Nov 2025 05:14:23 +0300 Subject: [PATCH 5/5] feat: GetUsertitles implemented --- api/_build/openapi.yaml | 25 +++- api/api.gen.go | 36 ++++- api/paths/users-id-titles.yaml | 25 +++- modules/backend/handlers/common.go | 64 +++++---- modules/backend/handlers/titles.go | 110 ++++++++-------- modules/backend/handlers/users.go | 205 ++++++++++++++++++++--------- modules/backend/queries.sql | 72 ++++++---- sql/queries.sql.go | 202 ++++++++++++++-------------- 8 files changed, 456 insertions(+), 283 deletions(-) diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index 722b7af..b3eacb4 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -146,11 +146,17 @@ paths: summary: Get user titles parameters: - $ref: '#/components/parameters/cursor' + - $ref: '#/components/parameters/title_sort' - in: path name: user_id required: true schema: type: string + - in: query + name: sort_forward + schema: + type: boolean + default: true - in: query name: word schema: @@ -173,6 +179,11 @@ paths: schema: type: number format: double + - in: query + name: my_rate + schema: + type: integer + format: int32 - in: query name: release_year schema: @@ -199,9 +210,17 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/UserTitle' + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/UserTitle' + cursor: + $ref: '#/components/schemas/CursorObj' + required: + - data + - cursor '204': description: No titles found '400': diff --git a/api/api.gen.go b/api/api.gen.go index 54c8fc1..e1f94c2 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -183,13 +183,16 @@ type GetUsersUserIdParams struct { // GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles. type GetUsersUserIdTitlesParams struct { - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - Word *string `form:"word,omitempty" json:"word,omitempty"` + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` + Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` + SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` + Word *string `form:"word,omitempty" json:"word,omitempty"` // Status List of title statuses to filter Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"` WatchStatus *UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"` Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` + MyRate *int32 `form:"my_rate,omitempty" json:"my_rate,omitempty"` ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` @@ -803,6 +806,22 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { return } + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort", c.Request.URL.Query(), ¶ms.Sort) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort: %w", err), http.StatusBadRequest) + return + } + + // ------------- Optional query parameter "sort_forward" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort_forward", c.Request.URL.Query(), ¶ms.SortForward) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort_forward: %w", err), http.StatusBadRequest) + return + } + // ------------- Optional query parameter "word" ------------- err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), ¶ms.Word) @@ -835,6 +854,14 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { return } + // ------------- Optional query parameter "my_rate" ------------- + + err = runtime.BindQueryParameter("form", true, false, "my_rate", c.Request.URL.Query(), ¶ms.MyRate) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter my_rate: %w", err), http.StatusBadRequest) + return + } + // ------------- Optional query parameter "release_year" ------------- err = runtime.BindQueryParameter("form", true, false, "release_year", c.Request.URL.Query(), ¶ms.ReleaseYear) @@ -1057,7 +1084,10 @@ type GetUsersUserIdTitlesResponseObject interface { VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error } -type GetUsersUserIdTitles200JSONResponse []UserTitle +type GetUsersUserIdTitles200JSONResponse struct { + Cursor CursorObj `json:"cursor"` + Data []UserTitle `json:"data"` +} func (response GetUsersUserIdTitles200JSONResponse) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 0788319..91b212d 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -2,11 +2,17 @@ get: summary: Get user titles parameters: - $ref: '../parameters/cursor.yaml' + - $ref: "../parameters/title_sort.yaml" - in: path name: user_id required: true schema: type: string + - in: query + name: sort_forward + schema: + type: boolean + default: true - in: query name: word schema: @@ -29,6 +35,11 @@ get: schema: type: number format: double + - in: query + name: my_rate + schema: + type: integer + format: int32 - in: query name: release_year schema: @@ -55,9 +66,17 @@ get: content: application/json: schema: - type: array - items: - $ref: '../schemas/UserTitle.yaml' + type: object + properties: + data: + type: array + items: + $ref: '../schemas/UserTitle.yaml' + cursor: + $ref: '../schemas/CursorObj.yaml' + required: + - data + - cursor '204': description: No titles found '400': diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 6618d49..89a3d59 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -17,7 +17,24 @@ func NewServer(db *sqlc.Queries) Server { return Server{db: db} } -func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi.Title, error) { +func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi.Title, error) { + + oapi_title := oapi.Title{ + EpisodesAired: title.EpisodesAired, + EpisodesAll: title.EpisodesAired, + // EpisodesLen: &episodes_lens, + Id: title.ID, + // Poster: &oapi_image, + Rating: title.Rating, + RatingCount: title.RatingCount, + // ReleaseSeason: &release_season, + ReleaseYear: title.ReleaseYear, + // Studio: &oapi_studio, + // Tags: oapi_tag_names, + // TitleNames: title_names, + // TitleStatus: oapi_status, + // AdditionalProperties: + } title_names := make(map[string][]string, 0) err := json.Unmarshal(title.TitleNames, &title_names) @@ -25,10 +42,13 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi. return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) } - episodes_lens := make(map[string]float64, 0) - err = json.Unmarshal(title.EpisodesLen, &episodes_lens) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) + if title.EpisodesLen != nil && len(title.EpisodesLen) > 0 { + episodes_lens := make(map[string]float64, 0) + err = json.Unmarshal(title.EpisodesLen, &episodes_lens) + if err != nil { + return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) + } + oapi_title.EpisodesLen = &episodes_lens } oapi_tag_names := make(oapi.Tags, 0) @@ -38,17 +58,19 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi. } var oapi_studio oapi.Studio - - oapi_studio.Id = title.StudioID if title.StudioName != nil { oapi_studio.Name = *title.StudioName } - oapi_studio.Description = title.StudioDesc - if title.StudioIllustID != nil { - oapi_studio.Poster.Id = title.StudioIllustID - oapi_studio.Poster.ImagePath = title.StudioImagePath - oapi_studio.Poster.StorageType = &title.StudioStorageType + if title.StudioID != 0 { + oapi_studio.Id = title.StudioID + oapi_studio.Description = title.StudioDesc + if title.StudioIllustID != nil { + oapi_studio.Poster.Id = title.StudioIllustID + oapi_studio.Poster.ImagePath = title.StudioImagePath + oapi_studio.Poster.StorageType = &title.StudioStorageType + } } + oapi_title.Studio = &oapi_studio var oapi_image oapi.Image @@ -62,27 +84,13 @@ func (s Server) mapTitle(ctx context.Context, title sqlc.SearchTitlesRow) (oapi. if title.ReleaseSeason != nil { release_season = oapi.ReleaseSeason(*title.ReleaseSeason) } + oapi_title.ReleaseSeason = &release_season oapi_status, err := TitleStatus2oapi(&title.TitleStatus) if err != nil { return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) } - oapi_title := oapi.Title{ - EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAired, - EpisodesLen: &episodes_lens, - Id: title.ID, - Poster: &oapi_image, - Rating: title.Rating, - RatingCount: title.RatingCount, - ReleaseSeason: &release_season, - ReleaseYear: title.ReleaseYear, - Studio: &oapi_studio, - Tags: oapi_tag_names, - TitleNames: title_names, - TitleStatus: oapi_status, - // AdditionalProperties: - } + oapi_title.TitleStatus = oapi_status return oapi_title, nil } diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index 78323f6..d593314 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -26,25 +26,25 @@ type SqlcStatus struct { planned string } -func TitleStatus2Sqlc(s *[]oapi.TitleStatus) (*SqlcStatus, error) { - var sqlc_status SqlcStatus - if s == nil { - return &sqlc_status, nil - } - for _, t := range *s { - switch t { - case oapi.TitleStatusFinished: - sqlc_status.finished = "finished" - case oapi.TitleStatusOngoing: - sqlc_status.ongoing = "ongoing" - case oapi.TitleStatusPlanned: - sqlc_status.planned = "planned" - default: - return nil, fmt.Errorf("unexpected tittle status: %s", t) - } - } - return &sqlc_status, nil -} +// func TitleStatus2Sqlc(s *[]oapi.TitleStatus) (*SqlcStatus, error) { +// var sqlc_status SqlcStatus +// if s == nil { +// return &sqlc_status, nil +// } +// for _, t := range *s { +// switch t { +// case oapi.TitleStatusFinished: +// sqlc_status.finished = "finished" +// case oapi.TitleStatusOngoing: +// sqlc_status.ongoing = "ongoing" +// case oapi.TitleStatusPlanned: +// sqlc_status.planned = "planned" +// default: +// return nil, fmt.Errorf("unexpected tittle status: %s", t) +// } +// } +// return &sqlc_status, nil +// } func TitleStatus2oapi(s *sqlc.TitleStatusT) (*oapi.TitleStatus, error) { if s == nil { @@ -169,29 +169,8 @@ func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitl log.Errorf("%v", err) return oapi.GetTitlesTitleId500Response{}, nil } - _sqlc_title := sqlc.SearchTitlesRow{ - ID: sqlc_title.ID, - StudioID: sqlc_title.StudioID, - PosterID: sqlc_title.PosterID, - TitleStatus: sqlc_title.TitleStatus, - Rating: sqlc_title.Rating, - RatingCount: sqlc_title.RatingCount, - ReleaseYear: sqlc_title.ReleaseYear, - ReleaseSeason: sqlc_title.ReleaseSeason, - Season: sqlc_title.Season, - EpisodesAired: sqlc_title.EpisodesAired, - EpisodesAll: sqlc_title.EpisodesAll, - EpisodesLen: sqlc_title.EpisodesLen, - TitleStorageType: sqlc_title.TitleStorageType, - TitleImagePath: sqlc_title.TitleImagePath, - TagNames: sqlc_title.TitleNames, - StudioName: sqlc_title.StudioName, - StudioIllustID: sqlc_title.StudioIllustID, - StudioDesc: sqlc_title.StudioDesc, - StudioStorageType: sqlc_title.StudioStorageType, - StudioImagePath: sqlc_title.StudioImagePath, - } - oapi_title, err = s.mapTitle(ctx, _sqlc_title) + + oapi_title, err = s.mapTitle(ctx, sqlc_title) if err != nil { log.Errorf("%v", err) return oapi.GetTitlesTitleId500Response{}, nil @@ -204,11 +183,6 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje opai_titles := make([]oapi.Title, 0) word := Word2Sqlc(request.Params.Word) - status, err := TitleStatus2Sqlc(request.Params.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles400Response{}, err - } season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) if err != nil { @@ -216,16 +190,22 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje return oapi.GetTitles400Response{}, err } + var statuses_sort []string + if request.Params.Status != nil { + for _, s := range *request.Params.Status { + ss := string(s) // s type is alias for string + statuses_sort = append(statuses_sort, ss) + } + } + params := sqlc.SearchTitlesParams{ Word: word, - Ongoing: status.ongoing, - Finished: status.finished, - Planned: status.planned, + TitleStatuses: statuses_sort, Rating: request.Params.Rating, ReleaseYear: request.Params.ReleaseYear, ReleaseSeason: season, - Forward: true, - SortBy: "id", + Forward: true, // default + SortBy: "id", // default Limit: request.Params.Limit, } @@ -235,6 +215,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje if request.Params.Sort != nil { params.SortBy = string(*request.Params.Sort) if request.Params.Cursor != nil { + // here we set CursorYear CursorID CursorRating fields err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) if err != nil { log.Errorf("%v", err) @@ -256,7 +237,30 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje for _, title := range titles { - t, err := s.mapTitle(ctx, title) + _title := sqlc.GetTitleByIDRow{ + ID: title.ID, + // StudioID: title.StudioID, + PosterID: title.PosterID, + TitleStatus: title.TitleStatus, + Rating: title.Rating, + RatingCount: title.RatingCount, + ReleaseYear: title.ReleaseYear, + ReleaseSeason: title.ReleaseSeason, + Season: title.Season, + EpisodesAired: title.EpisodesAired, + EpisodesAll: title.EpisodesAll, + // EpisodesLen: title.EpisodesLen, + TitleStorageType: title.TitleStorageType, + TitleImagePath: title.TitleImagePath, + TagNames: title.TitleNames, + StudioName: title.StudioName, + // StudioIllustID: title.StudioIllustID, + // StudioDesc: title.StudioDesc, + // StudioStorageType: title.StudioStorageType, + // StudioImagePath: title.StudioImagePath, + } + + t, err := s.mapTitle(ctx, _title) if err != nil { log.Errorf("%v", err) return oapi.GetTitles500Response{}, nil diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index 1ea2c1a..3223389 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -5,6 +5,7 @@ import ( "fmt" oapi "nyanimedb/api" sqlc "nyanimedb/sql" + "strconv" "time" "github.com/jackc/pgx/v5" @@ -53,8 +54,12 @@ func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdReque return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil } -func sqlDate2oapi(p_date pgtype.Timestamptz) (time.Time, error) { - return time.Time{}, nil +func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time { + if p_date.Valid { + t := p_date.Time + return &t + } + return nil } type SqlcUserStatus struct { @@ -64,26 +69,94 @@ type SqlcUserStatus struct { in_progress string } -func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { - var sqlc_status SqlcUserStatus - if s == nil { - return &sqlc_status, nil +// func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { +// var sqlc_status SqlcUserStatus +// if s == nil { +// return &sqlc_status, nil +// } +// for _, t := range *s { +// switch t { +// case oapi.UserTitleStatusFinished: +// sqlc_status.finished = "finished" +// case oapi.UserTitleStatusDropped: +// sqlc_status.dropped = "dropped" +// case oapi.UserTitleStatusPlanned: +// sqlc_status.planned = "planned" +// case oapi.UserTitleStatusInProgress: +// sqlc_status.in_progress = "in-progress" +// default: +// return nil, fmt.Errorf("unexpected tittle status: %s", t) +// } +// } +// return &sqlc_status, nil +// } + +func sql2usertitlestatus(s sqlc.UsertitleStatusT) (oapi.UserTitleStatus, error) { + var status oapi.UserTitleStatus + + switch status { + case "finished": + status = oapi.UserTitleStatusFinished + case "dropped": + status = oapi.UserTitleStatusDropped + case "planned": + status = oapi.UserTitleStatusPlanned + case "in-progress": + status = oapi.UserTitleStatusInProgress + default: + return status, fmt.Errorf("unexpected tittle status: %s", s) } - for _, t := range *s { - switch t { - case oapi.UserTitleStatusFinished: - sqlc_status.finished = "finished" - case oapi.UserTitleStatusDropped: - sqlc_status.dropped = "dropped" - case oapi.UserTitleStatusPlanned: - sqlc_status.planned = "planned" - case oapi.UserTitleStatusInProgress: - sqlc_status.in_progress = "in-progress" - default: - return nil, fmt.Errorf("unexpected tittle status: %s", t) - } + + return status, nil +} + +func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) { + + oapi_usertitle := oapi.UserTitle{ + Ctime: sqlDate2oapi(t.UserCtime), + Rate: t.UserRate, + ReviewId: t.ReviewID, + // Status: , + // Title: , + UserId: t.UserID, } - return &sqlc_status, nil + + status, err := sql2usertitlestatus(t.UsertitleStatus) + if err != nil { + return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) + } + oapi_usertitle.Status = status + + _title := sqlc.GetTitleByIDRow{ + ID: t.ID, + // StudioID: title.StudioID, + PosterID: t.PosterID, + TitleStatus: t.TitleStatus, + Rating: t.Rating, + RatingCount: t.RatingCount, + ReleaseYear: t.ReleaseYear, + ReleaseSeason: t.ReleaseSeason, + Season: t.Season, + EpisodesAired: t.EpisodesAired, + EpisodesAll: t.EpisodesAll, + // EpisodesLen: title.EpisodesLen, + TitleStorageType: t.TitleStorageType, + TitleImagePath: t.TitleImagePath, + TagNames: t.TitleNames, + StudioName: t.StudioName, + // StudioIllustID: title.StudioIllustID, + // StudioDesc: title.StudioDesc, + // StudioStorageType: title.StudioStorageType, + // StudioImagePath: title.StudioImagePath, + } + + oapi_title, err := s.mapTitle(ctx, _title) + if err != nil { + return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) + } + oapi_usertitle.Title = &oapi_title + + return oapi_usertitle, nil } func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersUserIdTitlesRequestObject) (oapi.GetUsersUserIdTitlesResponseObject, error) { @@ -91,11 +164,6 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU oapi_usertitles := make([]oapi.UserTitle, 0) word := Word2Sqlc(request.Params.Word) - status, err := TitleStatus2Sqlc(request.Params.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUsersUserIdTitles400Response{}, err - } season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) if err != nil { @@ -103,23 +171,33 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU return oapi.GetUsersUserIdTitles400Response{}, err } + var statuses_sort []string + if request.Params.Status != nil { + for _, s := range *request.Params.Status { + ss := string(s) // s type is alias for string + statuses_sort = append(statuses_sort, ss) + } + } + + var watch_status []string + if request.Params.WatchStatus != nil { + for _, s := range *request.Params.WatchStatus { + ss := string(s) // s type is alias for string + watch_status = append(statuses_sort, ss) + } + } + params := sqlc.SearchUserTitlesParams{ - Forward: true, - SortBy: "id", - // CursorYear : - // CursorID *int64 `json:"cursor_id"` - // CursorRating *float64 `json:"cursor_rating"` - // Word *string `json:"word"` - // Ongoing string `json:"ongoing"` - // Planned string `json:"planned"` - // Dropped string `json:"dropped"` - // InProgress string `json:"in-progress"` - // Finished string `json:"finished"` - // Rate *int32 `json:"rate"` - // Rating *float64 `json:"rating"` - // ReleaseYear *int32 `json:"release_year"` - // ReleaseSeason *ReleaseSeasonT `json:"release_season"` - // Limit *int32 `json:"limit"` + Word: word, + TitleStatuses: statuses_sort, + UsertitleStatuses: watch_status, + Rating: request.Params.Rating, + Rate: request.Params.MyRate, + ReleaseYear: request.Params.ReleaseYear, + ReleaseSeason: season, + Forward: true, // default + SortBy: "id", // default + Limit: request.Params.Limit, } if request.Params.SortForward != nil { @@ -128,6 +206,7 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU if request.Params.Sort != nil { params.SortBy = string(*request.Params.Sort) if request.Params.Cursor != nil { + // here we set CursorYear CursorID CursorRating fields err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) if err != nil { log.Errorf("%v", err) @@ -144,30 +223,30 @@ func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersU if len(titles) == 0 { return oapi.GetUsersUserIdTitles204Response{}, nil } + + var new_cursor oapi.CursorObj + for _, title := range titles { - _sqlc_title := sqlc.SearchTitlesRow{ - ID: sqlc_title.ID, - StudioID: sqlc_title.StudioID, - PosterID: sqlc_title.PosterID, - TitleStatus: sqlc_title.TitleStatus, - Rating: sqlc_title.Rating, - RatingCount: sqlc_title.RatingCount, - ReleaseYear: sqlc_title.ReleaseYear, - ReleaseSeason: sqlc_title.ReleaseSeason, - Season: sqlc_title.Season, - EpisodesAired: sqlc_title.EpisodesAired, - EpisodesAll: sqlc_title.EpisodesAll, - EpisodesLen: sqlc_title.EpisodesLen, - TitleStorageType: sqlc_title.TitleStorageType, - TitleImagePath: sqlc_title.TitleImagePath, - TagNames: sqlc_title.TitleNames, - StudioName: sqlc_title.StudioName, - StudioIllustID: sqlc_title.StudioIllustID, - StudioDesc: sqlc_title.StudioDesc, - StudioStorageType: sqlc_title.StudioStorageType, - StudioImagePath: sqlc_title.StudioImagePath, + + t, err := s.mapUsertitle(ctx, title) + if err != nil { + log.Errorf("%v", err) + return oapi.GetUsersUserIdTitles500Response{}, nil + } + oapi_usertitles = append(oapi_usertitles, t) + + new_cursor.Id = t.Title.Id + if request.Params.Sort != nil { + switch string(*request.Params.Sort) { + case "year": + tmp := fmt.Sprint(*t.Title.ReleaseYear) + new_cursor.Param = &tmp + case "rating": + tmp := strconv.FormatFloat(*t.Title.Rating, 'f', -1, 64) + new_cursor.Param = &tmp + } } } - return oapi.GetUsersUserIdTitles200JSONResponse(userTitles), nil + return oapi.GetUsersUserIdTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil } diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index ee2a84a..9734ff2 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -101,25 +101,30 @@ GROUP BY -- name: SearchTitles :many SELECT - t.*, + t.id as id, + t.title_names as title_names, + t.poster_id as poster_id, + t.title_status as title_status, + t.rating as rating, + t.rating_count as rating_count, + t.release_year as release_year, + t.release_season as release_season, + t.season as season, + t.episodes_aired as episodes_aired, + t.episodes_all as episodes_all, i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), '[]'::jsonb )::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type::text as studio_storage_type, - si.image_path as studio_image_path + s.studio_name as studio_name FROM titles as t LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) WHERE CASE @@ -199,7 +204,7 @@ WHERE AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) GROUP BY - t.id, i.id, s.id, si.id + t.id, i.id, s.id ORDER BY CASE WHEN sqlc.arg('forward')::boolean THEN @@ -223,7 +228,17 @@ LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit -- name: SearchUserTitles :many SELECT - t.*, + t.id as id, + t.title_names as title_names, + t.poster_id as poster_id, + t.title_status as title_status, + t.rating as rating, + t.rating_count as rating_count, + t.release_year as release_year, + t.release_season as release_season, + t.season as season, + t.episodes_aired as episodes_aired, + t.episodes_all as episodes_all, u.user_id as user_id, u.status as usertitle_status, u.rate as user_rate, @@ -231,20 +246,18 @@ SELECT u.ctime as user_ctime, i.storage_type::text as title_storage_type, i.image_path as title_image_path, - jsonb_agg(g.tag_name)'[]'::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type::text as studio_storage_type, - si.image_path as studio_image_path + COALESCE( + jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), + '[]'::jsonb + )::jsonb as tag_names, + s.studio_name as studio_name -FROM usertitles as u -LEFT JOIN titles as t ON (u.title_id = t.id) -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) +FROM usertitles as u +JOIN titles as t ON (u.title_id = t.id) +LEFT JOIN images as i ON (t.poster_id = i.id) +LEFT JOIN title_tags as tt ON (t.id = tt.title_id) +LEFT JOIN tags as g ON (tt.tag_id = g.id) +LEFT JOIN studios as s ON (t.studio_id = s.id) WHERE CASE @@ -312,15 +325,24 @@ WHERE END ) - AND (u.status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('planned')::text, sqlc.arg('dropped')::text, sqlc.arg('in-progress')::text)) - AND (t.title_status::text IN (sqlc.arg('ongoing')::text, sqlc.arg('finished')::text, sqlc.arg('planned')::text)) + AND ( + -- Если массив пуст (NULL или []) — не фильтруем + cardinality(sqlc.arg('title_statuses')::text[]) = 0 + OR + t.title_status = ANY(sqlc.arg('title_statuses')::text[]) + ) + AND ( + cardinality(sqlc.arg('usertitle_statuses')::text[]) = 0 + OR + u.status = ANY(sqlc.arg('usertitle_statuses')::text[]) + ) AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) GROUP BY - t.id, i.id, s.id, si.id + t.id, i.id, s.id ORDER BY CASE WHEN sqlc.arg('forward')::boolean THEN diff --git a/sql/queries.sql.go b/sql/queries.sql.go index bf2f08a..f7535db 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -342,25 +342,30 @@ func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams const searchTitles = `-- name: SearchTitles :many 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 as id, + t.title_names as title_names, + t.poster_id as poster_id, + t.title_status as title_status, + t.rating as rating, + t.rating_count as rating_count, + t.release_year as release_year, + t.release_season as release_season, + t.season as season, + t.episodes_aired as episodes_aired, + t.episodes_all as episodes_all, i.storage_type::text as title_storage_type, i.image_path as title_image_path, COALESCE( jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), '[]'::jsonb )::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type::text as studio_storage_type, - si.image_path as studio_image_path + s.studio_name as studio_name FROM titles as t LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) WHERE CASE @@ -440,7 +445,7 @@ WHERE AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t) GROUP BY - t.id, i.id, s.id, si.id + t.id, i.id, s.id ORDER BY CASE WHEN $1::boolean THEN @@ -478,27 +483,21 @@ type SearchTitlesParams struct { } type SearchTitlesRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - StudioID int64 `json:"studio_id"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - EpisodesLen []byte `json:"episodes_len"` - TitleStorageType string `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` - StudioIllustID *int64 `json:"studio_illust_id"` - StudioDesc *string `json:"studio_desc"` - StudioStorageType string `json:"studio_storage_type"` - StudioImagePath *string `json:"studio_image_path"` + ID int64 `json:"id"` + TitleNames json.RawMessage `json:"title_names"` + PosterID *int64 `json:"poster_id"` + TitleStatus TitleStatusT `json:"title_status"` + Rating *float64 `json:"rating"` + RatingCount *int32 `json:"rating_count"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Season *int32 `json:"season"` + EpisodesAired *int32 `json:"episodes_aired"` + EpisodesAll *int32 `json:"episodes_all"` + TitleStorageType string `json:"title_storage_type"` + TitleImagePath *string `json:"title_image_path"` + TagNames json.RawMessage `json:"tag_names"` + StudioName *string `json:"studio_name"` } func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]SearchTitlesRow, error) { @@ -525,7 +524,6 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S if err := rows.Scan( &i.ID, &i.TitleNames, - &i.StudioID, &i.PosterID, &i.TitleStatus, &i.Rating, @@ -535,15 +533,10 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S &i.Season, &i.EpisodesAired, &i.EpisodesAll, - &i.EpisodesLen, &i.TitleStorageType, &i.TitleImagePath, &i.TagNames, &i.StudioName, - &i.StudioIllustID, - &i.StudioDesc, - &i.StudioStorageType, - &i.StudioImagePath, ); err != nil { return nil, err } @@ -558,7 +551,17 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S const searchUserTitles = `-- name: SearchUserTitles :many 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 as id, + t.title_names as title_names, + t.poster_id as poster_id, + t.title_status as title_status, + t.rating as rating, + t.rating_count as rating_count, + t.release_year as release_year, + t.release_season as release_season, + t.season as season, + t.episodes_aired as episodes_aired, + t.episodes_all as episodes_all, u.user_id as user_id, u.status as usertitle_status, u.rate as user_rate, @@ -566,20 +569,18 @@ SELECT u.ctime as user_ctime, i.storage_type::text as title_storage_type, i.image_path as title_image_path, - jsonb_agg(g.tag_name)'[]'::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type::text as studio_storage_type, - si.image_path as studio_image_path + COALESCE( + jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), + '[]'::jsonb + )::jsonb as tag_names, + s.studio_name as studio_name FROM usertitles as u -LEFT JOIN titles as t ON (u.title_id = t.id) +JOIN titles as t ON (u.title_id = t.id) LEFT JOIN images as i ON (t.poster_id = i.id) LEFT JOIN title_tags as tt ON (t.id = tt.title_id) LEFT JOIN tags as g ON (tt.tag_id = g.id) LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) WHERE CASE @@ -647,15 +648,24 @@ WHERE END ) - AND (u.status::text IN ($7::text, $8::text, $9::text, $10::text)) - AND (t.title_status::text IN ($7::text, $11::text, $8::text)) - AND ($12::int IS NULL OR u.rate >= $12::int) - AND ($13::float IS NULL OR t.rating >= $13::float) - AND ($14::int IS NULL OR t.release_year = $14::int) - AND ($15::release_season_t IS NULL OR t.release_season = $15::release_season_t) + AND ( + -- Если массив пуст (NULL или []) — не фильтруем + cardinality($7::text[]) = 0 + OR + t.title_status = ANY($7::text[]) + ) + AND ( + cardinality($8::text[]) = 0 + OR + u.status = ANY($8::text[]) + ) + AND ($9::int IS NULL OR u.rate >= $9::int) + AND ($10::float IS NULL OR t.rating >= $10::float) + AND ($11::int IS NULL OR t.release_year = $11::int) + AND ($12::release_season_t IS NULL OR t.release_season = $12::release_season_t) GROUP BY - t.id, i.id, s.id, si.id + t.id, i.id, s.id ORDER BY CASE WHEN $1::boolean THEN @@ -677,55 +687,46 @@ ORDER BY CASE WHEN $2::text <> 'id' THEN t.id END ASC -LIMIT COALESCE($16::int, 100) +LIMIT COALESCE($13::int, 100) ` type SearchUserTitlesParams struct { - Forward bool `json:"forward"` - SortBy string `json:"sort_by"` - CursorYear *int32 `json:"cursor_year"` - CursorID *int64 `json:"cursor_id"` - CursorRating *float64 `json:"cursor_rating"` - Word *string `json:"word"` - Ongoing string `json:"ongoing"` - Planned string `json:"planned"` - Dropped string `json:"dropped"` - InProgress string `json:"in-progress"` - Finished string `json:"finished"` - Rate *int32 `json:"rate"` - Rating *float64 `json:"rating"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Limit *int32 `json:"limit"` + Forward bool `json:"forward"` + SortBy string `json:"sort_by"` + CursorYear *int32 `json:"cursor_year"` + CursorID *int64 `json:"cursor_id"` + CursorRating *float64 `json:"cursor_rating"` + Word *string `json:"word"` + TitleStatuses []string `json:"title_statuses"` + UsertitleStatuses []string `json:"usertitle_statuses"` + Rate *int32 `json:"rate"` + Rating *float64 `json:"rating"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Limit *int32 `json:"limit"` } type SearchUserTitlesRow struct { - ID *int64 `json:"id"` - TitleNames []byte `json:"title_names"` - StudioID *int64 `json:"studio_id"` - PosterID *int64 `json:"poster_id"` - TitleStatus *TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - EpisodesLen []byte `json:"episodes_len"` - UserID int64 `json:"user_id"` - UsertitleStatus UsertitleStatusT `json:"usertitle_status"` - UserRate *int32 `json:"user_rate"` - ReviewID *int64 `json:"review_id"` - UserCtime pgtype.Timestamptz `json:"user_ctime"` - TitleStorageType string `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` - StudioIllustID *int64 `json:"studio_illust_id"` - StudioDesc *string `json:"studio_desc"` - StudioStorageType string `json:"studio_storage_type"` - StudioImagePath *string `json:"studio_image_path"` + ID int64 `json:"id"` + TitleNames json.RawMessage `json:"title_names"` + PosterID *int64 `json:"poster_id"` + TitleStatus TitleStatusT `json:"title_status"` + Rating *float64 `json:"rating"` + RatingCount *int32 `json:"rating_count"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Season *int32 `json:"season"` + EpisodesAired *int32 `json:"episodes_aired"` + EpisodesAll *int32 `json:"episodes_all"` + UserID int64 `json:"user_id"` + UsertitleStatus UsertitleStatusT `json:"usertitle_status"` + UserRate *int32 `json:"user_rate"` + ReviewID *int64 `json:"review_id"` + UserCtime pgtype.Timestamptz `json:"user_ctime"` + TitleStorageType string `json:"title_storage_type"` + TitleImagePath *string `json:"title_image_path"` + TagNames json.RawMessage `json:"tag_names"` + StudioName *string `json:"studio_name"` } // 100 is default limit @@ -737,11 +738,8 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara arg.CursorID, arg.CursorRating, arg.Word, - arg.Ongoing, - arg.Planned, - arg.Dropped, - arg.InProgress, - arg.Finished, + arg.TitleStatuses, + arg.UsertitleStatuses, arg.Rate, arg.Rating, arg.ReleaseYear, @@ -758,7 +756,6 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara if err := rows.Scan( &i.ID, &i.TitleNames, - &i.StudioID, &i.PosterID, &i.TitleStatus, &i.Rating, @@ -768,7 +765,6 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara &i.Season, &i.EpisodesAired, &i.EpisodesAll, - &i.EpisodesLen, &i.UserID, &i.UsertitleStatus, &i.UserRate, @@ -778,10 +774,6 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara &i.TitleImagePath, &i.TagNames, &i.StudioName, - &i.StudioIllustID, - &i.StudioDesc, - &i.StudioStorageType, - &i.StudioImagePath, ); err != nil { return nil, err }