Compare commits

...

13 commits

Author SHA1 Message Date
fd8ecbeaca
Merge branch 'front' into dev
Some checks failed
Build and Deploy Go App / build (push) Failing after 11m25s
Build and Deploy Go App / deploy (push) Has been skipped
2025-11-19 10:55:25 +03:00
9c0fada00e fix: delete views column
All checks were successful
Build and Deploy Go App / build (push) Successful in 17m39s
Build and Deploy Go App / deploy (push) Successful in 3m34s
2025-11-19 04:16:23 +03:00
e2ac80610c fix: now gettitles must work
Some checks failed
Build and Deploy Go App / build (push) Has been cancelled
Build and Deploy Go App / deploy (push) Has been cancelled
2025-11-19 04:11:31 +03:00
7e6520c931 Merge branch 'dev' of ssh://meowgit.nekoea.red:22222/nihonium/nyanimedb into dev
Some checks failed
Build and Deploy Go App / build (push) Has been cancelled
Build and Deploy Go App / deploy (push) Has been cancelled
2025-11-19 03:59:45 +03:00
34d9341e75 feat: cursor stub added 2025-11-19 03:58:46 +03:00
fbf3f1d3a2 feat: now use _build to build 2025-11-19 03:57:44 +03:00
2025bb451f refact: openapi splitted into separate files 2025-11-19 03:14:41 +03:00
9ed09500ed
refact: slightly refactored openapi spec 2025-11-19 01:42:40 +03:00
c0be58ba07
feat: use pgxpool in backend
All checks were successful
Build and Deploy Go App / build (push) Successful in 17m56s
Build and Deploy Go App / deploy (push) Successful in 3m42s
2025-11-18 15:39:24 +03:00
a9391c18b9
fix: TitlesPage import path
All checks were successful
Build and Deploy Go App / build (push) Successful in 18m5s
Build and Deploy Go App / deploy (push) Successful in 3m36s
2025-11-18 15:30:11 +03:00
8504746d27
fix: updated package.json
All checks were successful
Build and Deploy Go App / build (push) Successful in 17m52s
Build and Deploy Go App / deploy (push) Successful in 4m19s
2025-11-18 05:39:11 +03:00
ecccc29aa8
fix
Some checks failed
Build and Deploy Go App / build (push) Has been cancelled
Build and Deploy Go App / deploy (push) Has been cancelled
2025-11-18 05:34:42 +03:00
7e41b6b9ce
fix
Some checks failed
Build and Deploy Go App / build (push) Failing after 11m5s
Build and Deploy Go App / deploy (push) Has been skipped
2025-11-18 05:26:01 +03:00
35 changed files with 1133 additions and 851 deletions

View file

@ -0,0 +1,6 @@
package: oapi
generate:
strict-server: true
gin-server: true
models: true
output: api/api.gen.go

436
api/_build/openapi.yaml Normal file
View file

@ -0,0 +1,436 @@
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

View file

@ -47,6 +47,12 @@ 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"`
@ -108,7 +114,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"`
AvatarId *int64 `json:"avatar_id,omitempty"`
// CreationDate Timestamp when the user was created
CreationDate *time.Time `json:"creation_date,omitempty"`
@ -906,7 +912,12 @@ type GetTitlesResponseObject interface {
VisitGetTitlesResponse(w http.ResponseWriter) error
}
type GetTitles200JSONResponse []Title
type GetTitles200JSONResponse struct {
Cursor CursorObj `json:"cursor"`
// Data List of titles
Data []Title `json:"data"`
}
func (response GetTitles200JSONResponse) VisitGetTitlesResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")

View file

@ -1,4 +1,4 @@
openapi: 3.1.1
openapi: 3.0.4
info:
title: Titles, Users, Reviews, Tags, and Media API
version: 1.0.0
@ -8,830 +8,15 @@ servers:
paths:
/titles:
get:
summary: Get titles
parameters:
- $ref: '#/components/parameters/cursor'
- $ref: '#/components/parameters/title_sort'
- in: query
name: sort_forward
default: true
schema:
type: boolean
- 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
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Title'
'204':
description: No titles found
'400':
description: Request params are not correct
'500':
description: Unknown server error
$ref: "./paths/titles.yaml"
/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'
'404':
description: Title not found
'400':
description: Request params are not correct
'500':
description: Unknown server error
'204':
description: No title found
# patch:
# summary: Update title info
# parameters:
# - in: path
# name: title_id
# required: true
# schema:
# type: string
# requestBody:
# required: true
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/Title'
# responses:
# '200':
# description: Update result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# user_json:
# $ref: '#/components/schemas/User'
# /titles/{title_id}/reviews:
# get:
# summary: Get title reviews
# parameters:
# - in: path
# name: title_id
# required: true
# schema:
# type: string
# - in: query
# name: limit
# schema:
# type: integer
# default: 10
# - in: query
# name: offset
# schema:
# type: integer
# default: 0
# responses:
# '200':
# description: List of reviews
# content:
# application/json:
# schema:
# type: array
# items:
# $ref: '#/components/schemas/Review'
# '204':
# description: No reviews found
$ref: "./paths/titles-id.yaml"
/users/{user_id}:
get:
summary: Get user info
parameters:
- in: path
name: user_id
required: true
schema:
type: string
- in: query
name: fields
schema:
type: string
default: all
responses:
'200':
description: User info
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
'400':
description: Request params are not correct
'500':
description: Unknown server error
# patch:
# summary: Update user
# parameters:
# - in: path
# name: user_id
# required: true
# schema:
# type: string
# requestBody:
# required: true
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/User'
# responses:
# '200':
# description: Update result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# delete:
# summary: Delete user
# parameters:
# - in: path
# name: user_id
# required: true
# schema:
# type: string
# responses:
# '200':
# description: Delete result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# /users:
# get:
# summary: Search user
# parameters:
# - in: query
# name: query
# schema:
# type: string
# - in: query
# name: fields
# schema:
# type: string
# responses:
# '200':
# description: List of users
# content:
# application/json:
# schema:
# type: array
# items:
# $ref: '#/components/schemas/User'
# post:
# summary: Add new user
# requestBody:
# required: true
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/User'
# responses:
# '200':
# description: Add result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# user_json:
# $ref: '#/components/schemas/User'
$ref: "./paths/users-id.yaml"
/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
# post:
# summary: Add user title
# parameters:
# - in: path
# name: user_id
# required: true
# schema:
# type: string
# requestBody:
# required: true
# content:
# application/json:
# schema:
# type: object
# properties:
# title_id:
# type: string
# status:
# type: string
# responses:
# '200':
# description: Add result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# patch:
# summary: Update user title
# parameters:
# - in: path
# name: user_id
# required: true
# schema:
# type: string
# requestBody:
# required: true
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/UserTitle'
# responses:
# '200':
# description: Update result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# delete:
# summary: Delete user title
# parameters:
# - in: path
# name: user_id
# required: true
# schema:
# type: string
# - in: query
# name: title_id
# schema:
# type: string
# responses:
# '200':
# description: Delete result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# /users/{user_id}/reviews:
# get:
# summary: Get user reviews
# parameters:
# - in: path
# name: user_id
# required: true
# schema:
# type: string
# - in: query
# name: limit
# schema:
# type: integer
# default: 10
# - in: query
# name: offset
# schema:
# type: integer
# default: 0
# responses:
# '200':
# description: List of reviews
# content:
# application/json:
# schema:
# type: array
# items:
# $ref: '#/components/schemas/Review'
# /reviews:
# post:
# summary: Add review
# requestBody:
# required: true
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/Review'
# responses:
# '200':
# description: Add result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# /reviews/{review_id}:
# patch:
# summary: Update review
# parameters:
# - in: path
# name: review_id
# required: true
# schema:
# type: string
# requestBody:
# required: true
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/Review'
# responses:
# '200':
# description: Update result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# delete:
# summary: Delete review
# parameters:
# - in: path
# name: review_id
# required: true
# schema:
# type: string
# responses:
# '200':
# description: Delete result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# /tags:
# get:
# summary: Get tags
# parameters:
# - in: query
# name: limit
# schema:
# type: integer
# default: 10
# - in: query
# name: offset
# schema:
# type: integer
# default: 0
# - in: query
# name: fields
# schema:
# type: string
# responses:
# '200':
# description: List of tags
# content:
# application/json:
# schema:
# type: array
# items:
# $ref: '#/components/schemas/Tag'
# /media:
# post:
# summary: Upload image
# responses:
# '200':
# description: Upload result
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# image_id:
# type: string
# get:
# summary: Get image path
# parameters:
# - in: query
# name: image_id
# required: true
# schema:
# type: string
# responses:
# '200':
# description: Image path
# content:
# application/json:
# schema:
# type: object
# properties:
# success:
# type: boolean
# error:
# type: string
# image_path:
# type: string
$ref: "./paths/users-id-titles.yaml"
components:
parameters:
cursor: # example base64( {id: 1, param: 2019})
in: query
name: cursor
required: false
schema:
type: string
title_sort:
in: query
name: sort
required: false
schema:
$ref: '#/components/schemas/TitleSort'
$ref: "./parameters/_index.yaml"
schemas:
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)
nullable: true
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
# - creation_date
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
$ref: "./schemas/_index.yaml"

View file

@ -0,0 +1,4 @@
cursor:
$ref: "./cursor.yaml"
title_sort:
$ref: "./title_sort.yaml"

View file

@ -0,0 +1,5 @@
in: query
name: cursor
required: false
schema:
type: string

View file

@ -0,0 +1,5 @@
in: query
name: sort
required: false
schema:
$ref: '../schemas/TitleSort.yaml'

29
api/paths/titles-id.yaml Normal file
View file

@ -0,0 +1,29 @@
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: "../schemas/Title.yaml"
'404':
description: Title not found
'400':
description: Request params are not correct
'500':
description: Unknown server error
'204':
description: No title found

73
api/paths/titles.yaml Normal file
View file

@ -0,0 +1,73 @@
get:
summary: Get titles
parameters:
- $ref: "../parameters/cursor.yaml"
- $ref: "../parameters/title_sort.yaml"
- in: query
name: sort_forward
schema:
type: boolean
default: true
- in: query
name: word
schema:
type: string
- in: query
name: status
schema:
$ref: '../schemas/enums/TitleStatus.yaml'
- 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: '../schemas/enums/ReleaseSeason.yaml'
- 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: '../schemas/Title.yaml'
description: List of titles
cursor:
$ref: '../schemas/CursorObj.yaml'
required:
- data
- cursor
'204':
description: No titles found
'400':
description: Request params are not correct
'500':
description: Unknown server error

View file

@ -0,0 +1,61 @@
get:
summary: Get user titles
parameters:
- $ref: '../parameters/cursor.yaml'
- in: path
name: user_id
required: true
schema:
type: string
- in: query
name: word
schema:
type: string
- in: query
name: status
schema:
$ref: '../schemas/enums/TitleStatus.yaml'
- in: query
name: watch_status
schema:
$ref: '../schemas/enums/UserTitleStatus.yaml'
- 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: '../schemas/enums/ReleaseSeason.yaml'
- 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: '../schemas/UserTitle.yaml'
'204':
description: No titles found
'400':
description: Request params are not correct
'500':
description: Unknown server error

26
api/paths/users-id.yaml Normal file
View file

@ -0,0 +1,26 @@
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: '../schemas/User.yaml'
'404':
description: User not found
'400':
description: Request params are not correct
'500':
description: Unknown server error

View file

@ -0,0 +1,9 @@
type: object
required:
- id
properties:
id:
type: integer
format: int64
param:
type: string

9
api/schemas/Image.yaml Normal file
View file

@ -0,0 +1,9 @@
type: object
properties:
id:
type: integer
format: int64
storage_type:
type: string
image_path:
type: string

2
api/schemas/Review.yaml Normal file
View file

@ -0,0 +1,2 @@
type: object
additionalProperties: true

14
api/schemas/Studio.yaml Normal file
View file

@ -0,0 +1,14 @@
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
poster:
$ref: ./Image.yaml
description:
type: string

8
api/schemas/Tag.yaml Normal file
View file

@ -0,0 +1,8 @@
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: 少女

11
api/schemas/Tags.yaml Normal file
View file

@ -0,0 +1,11 @@
type: array
description: Array of localized tags
items:
$ref: ./Tag.yaml
example:
- en: Shojo
ru: Сёдзё
ja: 少女
- en: Shounen
ru: Сёнен
ja: 少年

63
api/schemas/Title.yaml Normal file
View file

@ -0,0 +1,63 @@
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: ./Studio.yaml
tags:
$ref: ./Tags.yaml
poster:
$ref: ./Image.yaml
title_status:
$ref: ./enums/TitleStatus.yaml
rating:
type: number
format: double
rating_count:
type: integer
format: int32
release_year:
type: integer
format: int32
release_season:
$ref: ./enums/ReleaseSeason.yaml
episodes_aired:
type: integer
format: int32
episodes_all:
type: integer
format: int32
episodes_len:
type: object
additionalProperties:
type: number
format: double
additionalProperties: true

View file

@ -0,0 +1,8 @@
type: string
description: Title sort order
default: id
enum:
- id
- year
- rating
- views

40
api/schemas/User.yaml Normal file
View file

@ -0,0 +1,40 @@
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

View file

@ -0,0 +1,24 @@
type: object
required:
- user_id
- title_id
- status
properties:
user_id:
type: integer
format: int64
title_id:
type: integer
format: int64
status:
$ref: ./enums/UserTitleStatus.yaml
rate:
type: integer
format: int32
review_id:
type: integer
format: int64
ctime:
type: string
format: date-time
additionalProperties: true

26
api/schemas/_index.yaml Normal file
View file

@ -0,0 +1,26 @@
CursorObj:
$ref: ./CursorObj.yaml
TitleSort:
$ref: "./TitleSort.yaml"
Image:
$ref: "./Image.yaml"
TitleStatus:
$ref: "./enums/TitleStatus.yaml"
ReleaseSeason:
$ref: "./enums/ReleaseSeason.yaml"
UserTitleStatus:
$ref: "./enums/UserTitleStatus.yaml"
Review:
$ref: "./Review.yaml"
Tag:
$ref: "./Tag.yaml"
Tags:
$ref: "./Tags.yaml"
Studio:
$ref: "./Studio.yaml"
Title:
$ref: "./Title.yaml"
User:
$ref: "./User.yaml"
UserTitle:
$ref: "./UserTitle.yaml"

View file

@ -0,0 +1,7 @@
type: string
description: Title release season
enum:
- winter
- spring
- summer
- fall

View file

@ -0,0 +1,6 @@
type: string
description: Title status
enum:
- finished
- ongoing
- planned

View file

@ -0,0 +1,7 @@
type: string
description: User's title status
enum:
- finished
- planned
- dropped
- in-progress

5
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/jackc/pgx/v5 v5.7.6
github.com/oapi-codegen/runtime v1.1.2
github.com/pelletier/go-toml/v2 v2.2.4
golang.org/x/crypto v0.40.0
github.com/sirupsen/logrus v1.9.3
)
require (
@ -26,6 +26,7 @@ require (
github.com/google/uuid v1.5.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@ -34,11 +35,11 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect

View file

@ -218,6 +218,9 @@ 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)
@ -237,7 +240,8 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
Rating: request.Params.Rating,
ReleaseYear: request.Params.ReleaseYear,
ReleaseSeason: season,
Offset: request.Params.Offset,
Forward: true,
SortBy: "id",
Limit: request.Params.Limit,
})
if err != nil {
@ -258,5 +262,5 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje
opai_titles = append(opai_titles, t)
}
return oapi.GetTitles200JSONResponse(opai_titles), nil
return oapi.GetTitles200JSONResponse{Cursor: cursor, Data: opai_titles}, nil
}

View file

@ -13,7 +13,7 @@ import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/pelletier/go-toml/v2"
)
@ -31,17 +31,17 @@ func main() {
// log.Fatalf("Failed to init config: %v\n", err)
// }
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
os.Exit(1)
}
defer conn.Close(context.Background())
defer pool.Close()
r := gin.Default()
queries := sqlc.New(conn)
queries := sqlc.New(pool)
server := handlers.NewServer(queries)
// r.LoadHTMLGlob("templates/*")

View file

@ -107,9 +107,65 @@ 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 = '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 = '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,
LIMIT COALESCE(sqlc.narg('limit')::int, 100) -- 100 is default limit
OFFSET sqlc.narg('offset')::int;
-- Вторичный ключ: 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
-- -- name: ListTitles :many
-- SELECT title_id, title_names, studio_id, poster_id, signal_ids,

View file

@ -32,5 +32,8 @@
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
},
"engines": {
"node": "20.x"
}
}

View file

@ -20,7 +20,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},

View file

@ -18,7 +18,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},

View file

@ -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,

View file

@ -212,7 +212,6 @@ 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"`
@ -277,10 +276,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"`
ReviewText *string `json:"review_text"`
ReviewDate pgtype.Timestamptz `json:"review_date"`
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"`
}

View file

@ -8,6 +8,8 @@ package sqlc
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const createImage = `-- name: CreateImage :one
@ -31,7 +33,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
WHERE id = $1::bigint
`
func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, error) {
@ -44,11 +46,13 @@ func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, erro
const getReviewByID = `-- name: GetReviewByID :one
SELECT id, data, rating, illust_id, user_id, title_id, created_at
SELECT id, data, rating, 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,
//
@ -82,7 +86,6 @@ 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,
@ -312,9 +315,29 @@ 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 = 'id' THEN id
WHEN $6::boolean AND $7::text = 'year' THEN release_year
WHEN $6::boolean AND $7::text = 'rating' THEN rating
-- WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
END ASC,
CASE
WHEN NOT $6::boolean AND $7::text = 'id' THEN id
WHEN NOT $6::boolean AND $7::text = 'year' THEN release_year
WHEN NOT $6::boolean AND $7::text = 'rating' THEN rating
-- WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views
END DESC,
LIMIT COALESCE($7::int, 100) -- 100 is default limit
OFFSET $6::int
-- Вторичный ключ: id только если НЕ сортируем по id
CASE
WHEN $7::text != 'id' AND $6::boolean THEN id
END ASC,
CASE
WHEN $7::text != 'id' AND NOT $6::boolean THEN id
END DESC
LIMIT COALESCE($8::int, 100)
`
type SearchTitlesParams struct {
@ -323,7 +346,8 @@ type SearchTitlesParams struct {
Rating *float64 `json:"rating"`
ReleaseYear *int32 `json:"release_year"`
ReleaseSeason *ReleaseSeasonT `json:"release_season"`
Offset *int32 `json:"offset"`
Forward bool `json:"forward"`
SortBy string `json:"sort_by"`
Limit *int32 `json:"limit"`
}
@ -334,7 +358,8 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]T
arg.Rating,
arg.ReleaseYear,
arg.ReleaseSeason,
arg.Offset,
arg.Forward,
arg.SortBy,
arg.Limit,
)
if err != nil {
@ -368,3 +393,122 @@ 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
}