diff --git a/api/_build/oapi-codegen.yaml b/api/_build/oapi-codegen.yaml deleted file mode 100644 index 32e029a..0000000 --- a/api/_build/oapi-codegen.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: oapi -generate: - strict-server: true - gin-server: true - models: true -output: api/api.gen.go \ No newline at end of file diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml deleted file mode 100644 index 5ff77e0..0000000 --- a/api/_build/openapi.yaml +++ /dev/null @@ -1,436 +0,0 @@ -openapi: 3.0.4 -info: - title: 'Titles, Users, Reviews, Tags, and Media API' - version: 1.0.0 -servers: - - url: /api/v1 -paths: - /titles: - get: - summary: Get titles - parameters: - - $ref: '#/components/parameters/cursor' - - $ref: '#/components/parameters/title_sort' - - in: query - name: sort_forward - schema: - type: boolean - default: true - - in: query - name: word - schema: - type: string - - in: query - name: status - schema: - $ref: '#/components/schemas/TitleStatus' - - in: query - name: rating - schema: - type: number - format: double - - in: query - name: release_year - schema: - type: integer - format: int32 - - in: query - name: release_season - schema: - $ref: '#/components/schemas/ReleaseSeason' - - in: query - name: limit - schema: - type: integer - format: int32 - default: 10 - - in: query - name: offset - schema: - type: integer - format: int32 - default: 0 - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: List of titles with cursor - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/Title' - description: List of titles - cursor: - $ref: '#/components/schemas/CursorObj' - required: - - data - - cursor - '204': - description: No titles found - '400': - description: Request params are not correct - '500': - description: Unknown server error - '/titles/{title_id}': - get: - summary: Get title description - parameters: - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: Title description - content: - application/json: - schema: - $ref: '#/components/schemas/Title' - '204': - description: No title found - '400': - description: Request params are not correct - '404': - description: Title not found - '500': - description: Unknown server error - '/users/{user_id}': - get: - summary: Get user info - parameters: - - in: path - name: user_id - required: true - schema: - type: string - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: User info - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '400': - description: Request params are not correct - '404': - description: User not found - '500': - description: Unknown server error - '/users/{user_id}/titles/': - get: - summary: Get user titles - parameters: - - $ref: '#/components/parameters/cursor' - - in: path - name: user_id - required: true - schema: - type: string - - in: query - name: word - schema: - type: string - - in: query - name: status - schema: - $ref: '#/components/schemas/TitleStatus' - - in: query - name: watch_status - schema: - $ref: '#/components/schemas/UserTitleStatus' - - in: query - name: rating - schema: - type: number - format: double - - in: query - name: release_year - schema: - type: integer - format: int32 - - in: query - name: release_season - schema: - $ref: '#/components/schemas/ReleaseSeason' - - in: query - name: limit - schema: - type: integer - format: int32 - default: 10 - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: List of user titles - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/UserTitle' - '204': - description: No titles found - '400': - description: Request params are not correct - '500': - description: Unknown server error -components: - parameters: - cursor: - in: query - name: cursor - required: false - schema: - type: string - title_sort: - in: query - name: sort - required: false - schema: - $ref: '#/components/schemas/TitleSort' - schemas: - CursorObj: - type: object - required: - - id - properties: - id: - type: integer - format: int64 - param: - type: string - TitleSort: - type: string - description: Title sort order - default: id - enum: - - id - - year - - rating - - views - Image: - type: object - properties: - id: - type: integer - format: int64 - storage_type: - type: string - image_path: - type: string - TitleStatus: - type: string - description: Title status - enum: - - finished - - ongoing - - planned - ReleaseSeason: - type: string - description: Title release season - enum: - - winter - - spring - - summer - - fall - UserTitleStatus: - type: string - description: User's title status - enum: - - finished - - planned - - dropped - - in-progress - Review: - type: object - additionalProperties: true - Tag: - type: object - description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' - additionalProperties: - type: string - example: - en: Shojo - ru: Сёдзё - ja: 少女 - Tags: - type: array - description: Array of localized tags - items: - $ref: '#/components/schemas/Tag' - example: - - en: Shojo - ru: Сёдзё - ja: 少女 - - en: Shounen - ru: Сёнен - ja: 少年 - Studio: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string - poster: - $ref: '#/components/schemas/Image' - description: - type: string - Title: - type: object - required: - - id - - title_names - - tags - properties: - id: - type: integer - format: int64 - description: Unique title ID (primary key) - example: 1 - title_names: - type: object - description: 'Localized titles. Key = language (ISO 639-1), value = list of names' - additionalProperties: - type: array - items: - type: string - example: Attack on Titan - minItems: 1 - example: - - Attack on Titan - - AoT - example: - en: - - Attack on Titan - - AoT - ru: - - Атака титанов - - Титаны - ja: - - 進撃の巨人 - studio: - $ref: '#/components/schemas/Studio' - tags: - $ref: '#/components/schemas/Tags' - poster: - $ref: '#/components/schemas/Image' - title_status: - $ref: '#/components/schemas/TitleStatus' - rating: - type: number - format: double - rating_count: - type: integer - format: int32 - release_year: - type: integer - format: int32 - release_season: - $ref: '#/components/schemas/ReleaseSeason' - episodes_aired: - type: integer - format: int32 - episodes_all: - type: integer - format: int32 - episodes_len: - type: object - additionalProperties: - type: number - format: double - additionalProperties: true - User: - type: object - properties: - id: - type: integer - format: int64 - description: Unique user ID (primary key) - example: 1 - avatar_id: - type: integer - format: int64 - description: ID of the user avatar (references images table) - example: null - mail: - type: string - format: email - description: User email - example: john.doe@example.com - nickname: - type: string - description: Username (alphanumeric + _ or -) - maxLength: 16 - example: john_doe_42 - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Doe - user_desc: - type: string - description: User description - maxLength: 512 - example: Just a regular user. - creation_date: - type: string - format: date-time - description: Timestamp when the user was created - example: '2025-10-10T23:45:47.908073Z' - required: - - user_id - - nickname - UserTitle: - type: object - required: - - user_id - - title_id - - status - properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time - additionalProperties: true diff --git a/api/api.gen.go b/api/api.gen.go index f252a5a..427f5af 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -47,12 +47,6 @@ const ( UserTitleStatusPlanned UserTitleStatus = "planned" ) -// CursorObj defines model for CursorObj. -type CursorObj struct { - Id int64 `json:"id"` - Param *string `json:"param,omitempty"` -} - // Image defines model for Image. type Image struct { Id *int64 `json:"id,omitempty"` @@ -114,7 +108,7 @@ type TitleStatus string // User defines model for User. type User struct { // AvatarId ID of the user avatar (references images table) - AvatarId *int64 `json:"avatar_id,omitempty"` + AvatarId *int64 `json:"avatar_id"` // CreationDate Timestamp when the user was created CreationDate *time.Time `json:"creation_date,omitempty"` @@ -912,12 +906,7 @@ type GetTitlesResponseObject interface { VisitGetTitlesResponse(w http.ResponseWriter) error } -type GetTitles200JSONResponse struct { - Cursor CursorObj `json:"cursor"` - - // Data List of titles - Data []Title `json:"data"` -} +type GetTitles200JSONResponse []Title func (response GetTitles200JSONResponse) VisitGetTitlesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") diff --git a/api/paths/titles.yaml b/api/paths/titles.yaml index e868ed6..4fd010d 100644 --- a/api/paths/titles.yaml +++ b/api/paths/titles.yaml @@ -1,8 +1,8 @@ get: summary: Get titles parameters: - - $ref: "../parameters/cursor.yaml" - - $ref: "../parameters/title_sort.yaml" + - $ref: ../parameters/cursor.yaml + - $ref: ../parameters/title_sort.yaml - in: query name: sort_forward schema: diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index e8a3bff..46ff982 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -218,9 +218,6 @@ func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitl func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObject) (oapi.GetTitlesResponseObject, error) { opai_titles := make([]oapi.Title, 0) - cursor := oapi.CursorObj{ - Id: 1, - } word := Word2Sqlc(request.Params.Word) status, err := TitleStatus2Sqlc(request.Params.Status) @@ -240,6 +237,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje Rating: request.Params.Rating, ReleaseYear: request.Params.ReleaseYear, ReleaseSeason: season, + Offset: request.Params.Offset, Limit: request.Params.Limit, }) if err != nil { @@ -260,5 +258,5 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje opai_titles = append(opai_titles, t) } - return oapi.GetTitles200JSONResponse{Cursor: cursor, Data: opai_titles}, nil + return oapi.GetTitles200JSONResponse(opai_titles), nil } diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 8962895..423be37 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -107,67 +107,9 @@ WHERE AND (sqlc.narg('rating')::float IS NULL OR rating >= sqlc.narg('rating')::float) AND (sqlc.narg('release_year')::int IS NULL OR release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_season')::release_season_t IS NULL OR release_season = sqlc.narg('release_season')::release_season_t) -ORDER BY - -- Основной ключ: выбранное поле - CASE - WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'id' THEN id - WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'name' THEN name - WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'year' THEN release_year - WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'rating' THEN rating - WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views - END ASC, - CASE - WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'id' THEN id - WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'name' THEN name - WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'year' THEN release_year - WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'rating' THEN rating - WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views - END DESC, - -- Вторичный ключ: id — только если НЕ сортируем по id - CASE - WHEN sqlc.arg(sort_by)::text != 'id' AND sqlc.arg(forward)::boolean THEN id - END ASC, - CASE - WHEN sqlc.arg(sort_by)::text != 'id' AND NOT sqlc.arg(forward)::boolean THEN id - END DESC -LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit --- OFFSET sqlc.narg('offset')::int; - --- 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 - END - - AND (sqlc.narg('status')::title_status_t IS NULL OR t.title_status = sqlc.narg('status')::title_status_t) - 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) - -LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit +LIMIT COALESCE(sqlc.narg('limit')::int, 100) -- 100 is default limit +OFFSET sqlc.narg('offset')::int; -- -- name: ListTitles :many -- SELECT title_id, title_names, studio_id, poster_id, signal_ids, diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index e6ed628..49cca3d 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -59,7 +59,7 @@ CREATE TABLE studios ( ); CREATE TABLE titles ( - -- // TODO: anime type (film, season etc) + // TODO: anime type (film, season etc) id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]} title_names jsonb NOT NULL, diff --git a/sql/models.go b/sql/models.go index 93cecca..a593504 100644 --- a/sql/models.go +++ b/sql/models.go @@ -212,6 +212,7 @@ type Review struct { ID int64 `json:"id"` Data string `json:"data"` Rating *int32 `json:"rating"` + IllustID *int64 `json:"illust_id"` UserID *int64 `json:"user_id"` TitleID *int64 `json:"title_id"` CreatedAt pgtype.Timestamptz `json:"created_at"` @@ -276,10 +277,10 @@ type User struct { } type Usertitle 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"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` + Status UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + ReviewText *string `json:"review_text"` + ReviewDate pgtype.Timestamptz `json:"review_date"` } diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 4e28f40..c5e6f8a 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -8,8 +8,6 @@ package sqlc import ( "context" "time" - - "github.com/jackc/pgx/v5/pgtype" ) const createImage = `-- name: CreateImage :one @@ -33,7 +31,7 @@ func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Image const getImageByID = `-- name: GetImageByID :one SELECT id, storage_type, image_path FROM images -WHERE id = $1::bigint +WHERE id = $1 ` func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, error) { @@ -46,13 +44,11 @@ func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, erro const getReviewByID = `-- name: GetReviewByID :one - -SELECT id, data, rating, user_id, title_id, created_at +SELECT id, data, rating, illust_id, user_id, title_id, created_at FROM reviews WHERE review_id = $1::bigint ` -// 100 is default limit // -- name: ListTitles :many // SELECT title_id, title_names, studio_id, poster_id, signal_ids, // @@ -86,6 +82,7 @@ func (q *Queries) GetReviewByID(ctx context.Context, reviewID int64) (Review, er &i.ID, &i.Data, &i.Rating, + &i.IllustID, &i.UserID, &i.TitleID, &i.CreatedAt, @@ -315,18 +312,9 @@ WHERE AND ($3::float IS NULL OR rating >= $3::float) AND ($4::int IS NULL OR release_year = $4::int) AND ($5::release_season_t IS NULL OR release_season = $5::release_season_t) -ORDER BY CASE - WHEN $6::boolean AND $7::text = 'name' THEN name - WHEN $8::boolean AND $7::text = 'id' THEN id - WHEN $8::boolean AND $7::text = 'name' THEN name - WHEN $8::boolean AND $7::text = 'id' THEN id -END ASC, CASE - WHEN NOT $8::boolean AND $7::text = 'name' THEN name - WHEN NOT $8::boolean AND $7::text = 'id' THEN id - WHEN NOT $8::boolean AND $7::text = 'name' THEN name - WHEN NOT $8::boolean AND $7::text = 'id' THEN id -END DESC -LIMIT COALESCE($9::int, 100) + +LIMIT COALESCE($7::int, 100) -- 100 is default limit +OFFSET $6::int ` type SearchTitlesParams struct { @@ -335,9 +323,7 @@ type SearchTitlesParams struct { Rating *float64 `json:"rating"` ReleaseYear *int32 `json:"release_year"` ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Forward bool `json:"forward"` - OrderBy string `json:"order_by"` - Reverse bool `json:"reverse"` + Offset *int32 `json:"offset"` Limit *int32 `json:"limit"` } @@ -348,9 +334,7 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]T arg.Rating, arg.ReleaseYear, arg.ReleaseSeason, - arg.Forward, - arg.OrderBy, - arg.Reverse, + arg.Offset, arg.Limit, ) if err != nil { @@ -384,122 +368,3 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]T } return items, nil } - -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 - 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) - -LIMIT COALESCE($7::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"` -} - -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"` -} - -// 100 is default limit -// OFFSET sqlc.narg('offset')::int; -func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) { - rows, err := q.db.Query(ctx, searchUserTitles, - arg.Word, - arg.Status, - arg.Rating, - arg.ReleaseYear, - arg.ReleaseSeason, - arg.UsertitleStatus, - arg.Limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []SearchUserTitlesRow - 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, - &i.PosterID, - &i.TitleStatus, - &i.Rating, - &i.RatingCount, - &i.ReleaseYear, - &i.ReleaseSeason, - &i.Season, - &i.EpisodesAired, - &i.EpisodesAll, - &i.EpisodesLen, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -}