Compare commits

...

17 commits

Author SHA1 Message Date
6d538ed154 feat: common code for back functions are now in handlers/common.go 2025-11-01 21:17:42 +03:00
6ed47b667c
feat: proxy api requests via frontend
All checks were successful
Build and Deploy Go App / build (push) Successful in 8m23s
Build and Deploy Go App / deploy (push) Successful in 3m31s
2025-10-26 04:06:38 +03:00
66281838b2
fix: new psql feature
All checks were successful
Build and Deploy Go App / build (push) Successful in 8m46s
Build and Deploy Go App / deploy (push) Successful in 3m35s
2025-10-26 03:18:54 +03:00
feb763509e
fix: regenerated frontend openapi functions
All checks were successful
Build and Deploy Go App / build (push) Successful in 8m44s
Build and Deploy Go App / deploy (push) Successful in 3m24s
2025-10-26 02:51:53 +03:00
fd1e129a5f
fix: remove templates from cicd
Some checks failed
Build and Deploy Go App / build (push) Has been cancelled
Build and Deploy Go App / deploy (push) Has been cancelled
2025-10-26 02:49:50 +03:00
92c06bbb50
fix: remove templates from backend Dockerfile
Some checks failed
Build and Deploy Go App / build (push) Failing after 5m49s
Build and Deploy Go App / deploy (push) Has been skipped
2025-10-26 02:44:28 +03:00
e12812d202
feat: frontend first routing implementation
Some checks failed
Build and Deploy Go App / build (push) Failing after 6m8s
Build and Deploy Go App / deploy (push) Has been skipped
2025-10-26 02:39:54 +03:00
948e036e8c
feat: implemented /users/{id} api route 2025-10-26 02:34:45 +03:00
71e2661fb9
feat!: rewritten db scheme 2025-10-26 02:09:44 +03:00
db53ae04e3
refact!: project structure
Some checks failed
Build and Deploy Go App / build (push) Failing after 4m35s
Build and Deploy Go App / deploy (push) Has been skipped
2025-10-25 21:05:16 +03:00
fd0ca4411b
feat: created UserPage.tsx
All checks were successful
Build and Deploy Go App / build (push) Successful in 6m33s
Build and Deploy Go App / deploy (push) Successful in 2m52s
2025-10-11 05:24:11 +03:00
827431bb2f
fix: App.tsx user field names
All checks were successful
Build and Deploy Go App / build (push) Successful in 7m1s
Build and Deploy Go App / deploy (push) Successful in 2m55s
2025-10-11 04:54:39 +03:00
0b5ea285de
fix: regenerated api
Some checks failed
Build and Deploy Go App / build (push) Failing after 4m6s
Build and Deploy Go App / deploy (push) Has been skipped
2025-10-11 04:52:21 +03:00
a0df0dff00
feat: add User scahema to api 2025-10-11 04:51:50 +03:00
f2188f6363
fix: temp fix for API endpoint
All checks were successful
Build and Deploy Go App / build (push) Successful in 6m42s
Build and Deploy Go App / deploy (push) Successful in 2m53s
2025-10-11 04:33:56 +03:00
8e44b8b7b4
feat: fetch user info
All checks were successful
Build and Deploy Go App / build (push) Successful in 6m46s
Build and Deploy Go App / deploy (push) Successful in 2m54s
2025-10-11 04:25:57 +03:00
28c38ca1a0 feat: db url env added
All checks were successful
Build and Deploy Go App / build (push) Successful in 6m44s
Build and Deploy Go App / deploy (push) Successful in 2m47s
2025-10-11 02:27:13 +03:00
52 changed files with 2382 additions and 7250 deletions

View file

@ -27,7 +27,7 @@ jobs:
cd modules/backend
go mod tidy
go build -o nyanimedb .
tar -czvf nyanimedb-backend.tar.gz nyanimedb templates/
tar -czvf nyanimedb-backend.tar.gz nyanimedb
- name: Upload built backend to artifactory
uses: actions/upload-artifact@v3
@ -94,6 +94,7 @@ jobs:
POSTGRES_PORT: 5432
POSTGRES_VERSION: 18
LOG_LEVEL: ${{ vars.LOG_LEVEL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- name: Checkout code

View file

@ -2,6 +2,5 @@ FROM ubuntu:22.04
WORKDIR /app
COPY --chmod=755 modules/backend/nyanimedb /app
COPY modules/backend/templates /app/templates
EXPOSE 8080
ENTRYPOINT ["/app/nyanimedb"]

View file

@ -1,4 +1,5 @@
FROM nginx:alpine
COPY modules/frontend/dist /usr/share/nginx/html
COPY modules/frontend/nginx-default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

200
api/api.gen.go Normal file
View file

@ -0,0 +1,200 @@
// Package oapi provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT.
package oapi
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/oapi-codegen/runtime"
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
openapi_types "github.com/oapi-codegen/runtime/types"
)
// User defines model for User.
type User struct {
// AvatarId ID of the user avatar (references images table)
AvatarId *int64 `json:"avatar_id"`
// CreationDate Timestamp when the user was created
CreationDate time.Time `json:"creation_date"`
// DispName Display name
DispName *string `json:"disp_name,omitempty"`
// Id Unique user ID (primary key)
Id *int64 `json:"id,omitempty"`
// Mail User email
Mail *openapi_types.Email `json:"mail,omitempty"`
// Nickname Username (alphanumeric + _ or -)
Nickname string `json:"nickname"`
// UserDesc User description
UserDesc *string `json:"user_desc,omitempty"`
}
// GetUsersUserIdParams defines parameters for GetUsersUserId.
type GetUsersUserIdParams struct {
Fields *string `form:"fields,omitempty" json:"fields,omitempty"`
}
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Get user info
// (GET /users/{user_id})
GetUsersUserId(c *gin.Context, userId string, params GetUsersUserIdParams)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
ErrorHandler func(*gin.Context, error, int)
}
type MiddlewareFunc func(c *gin.Context)
// GetUsersUserId operation middleware
func (siw *ServerInterfaceWrapper) GetUsersUserId(c *gin.Context) {
var err error
// ------------- Path parameter "user_id" -------------
var userId string
err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true})
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest)
return
}
// Parameter object where we will unmarshal all parameters from the context
var params GetUsersUserIdParams
// ------------- Optional query parameter "fields" -------------
err = runtime.BindQueryParameter("form", true, false, "fields", c.Request.URL.Query(), &params.Fields)
if err != nil {
siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter fields: %w", err), http.StatusBadRequest)
return
}
for _, middleware := range siw.HandlerMiddlewares {
middleware(c)
if c.IsAborted() {
return
}
}
siw.Handler.GetUsersUserId(c, userId, params)
}
// GinServerOptions provides options for the Gin server.
type GinServerOptions struct {
BaseURL string
Middlewares []MiddlewareFunc
ErrorHandler func(*gin.Context, error, int)
}
// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.
func RegisterHandlers(router gin.IRouter, si ServerInterface) {
RegisterHandlersWithOptions(router, si, GinServerOptions{})
}
// RegisterHandlersWithOptions creates http.Handler with additional options
func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) {
errorHandler := options.ErrorHandler
if errorHandler == nil {
errorHandler = func(c *gin.Context, err error, statusCode int) {
c.JSON(statusCode, gin.H{"msg": err.Error()})
}
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
ErrorHandler: errorHandler,
}
router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId)
}
type GetUsersUserIdRequestObject struct {
UserId string `json:"user_id"`
Params GetUsersUserIdParams
}
type GetUsersUserIdResponseObject interface {
VisitGetUsersUserIdResponse(w http.ResponseWriter) error
}
type GetUsersUserId200JSONResponse User
func (response GetUsersUserId200JSONResponse) VisitGetUsersUserIdResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetUsersUserId404Response struct {
}
func (response GetUsersUserId404Response) VisitGetUsersUserIdResponse(w http.ResponseWriter) error {
w.WriteHeader(404)
return nil
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// Get user info
// (GET /users/{user_id})
GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error)
}
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
}
// GetUsersUserId operation middleware
func (sh *strictHandler) GetUsersUserId(ctx *gin.Context, userId string, params GetUsersUserIdParams) {
var request GetUsersUserIdRequestObject
request.UserId = userId
request.Params = params
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
return sh.ssi.GetUsersUserId(ctx, request.(GetUsersUserIdRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetUsersUserId")
}
response, err := handler(ctx, request)
if err != nil {
ctx.Error(err)
ctx.Status(http.StatusInternalServerError)
} else if validResponse, ok := response.(GetUsersUserIdResponseObject); ok {
if err := validResponse.VisitGetUsersUserIdResponse(ctx.Writer); err != nil {
ctx.Error(err)
}
} else if response != nil {
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
}
}

1958
api/gen.go

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ services:
ports:
- "${POSTGRES_PORT}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- postgres_data:/var/lib/postgresql
# pgadmin:
# image: dpage/pgadmin4:${PGADMIN_VERSION}
@ -32,6 +32,7 @@ services:
restart: always
environment:
LOG_LEVEL: ${LOG_LEVEL}
DATABASE_URL: ${DATABASE_URL}
ports:
- "8080:8080"
depends_on:

3
deploy/generate.sh Normal file
View file

@ -0,0 +1,3 @@
npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios
oapi-codegen --config=api/oapi-codegen.yaml .\api\openapi.yaml
sqlc generate -f .\sql\sqlc.yaml

View file

@ -1,16 +1,23 @@
module nyanimedb-server
module nyanimedb
go 1.25.0
require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
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
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/cors v1.7.6 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.11.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
@ -19,23 +26,18 @@ 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/pgx/v5 v5.7.6 // 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
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // 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

@ -9,9 +9,8 @@ github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
@ -20,18 +19,20 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -41,6 +42,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
@ -50,7 +53,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -60,13 +62,12 @@ github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQ
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@ -76,6 +77,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
@ -92,8 +95,6 @@ golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
@ -105,4 +106,5 @@ google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7I
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

File diff suppressed because it is too large Load diff

View file

@ -1,718 +0,0 @@
package api
import (
"context"
"encoding/json"
"nyanimedb-server/db"
"strconv"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt"
)
type Server struct {
db *db.Queries
}
func NewServer(db *db.Queries) Server {
return Server{db: db}
}
// —————————————————————————————————————————————
// ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
// —————————————————————————————————————————————
func parseInt32(s string) (int32, error) {
i, err := strconv.ParseInt(s, 10, 32)
return int32(i), err
}
func ptr[T any](v T) *T { return &v }
func pgInt4ToPtr(v pgtype.Int4) *int32 {
if v.Valid {
return &v.Int32
}
return nil
}
func pgTextToPtr(v pgtype.Text) *string {
if v.Valid {
return &v.String
}
return nil
}
func pgFloat8ToPtr(v pgtype.Float8) *float64 {
if v.Valid {
return &v.Float64
}
return nil
}
func jsonbToInterface(data []byte) interface{} {
if data == nil {
return nil
}
var out interface{}
if err := json.Unmarshal(data, &out); err != nil {
return string(data) // fallback
}
return out
}
// —————————————————————————————————————————————
// ХЕНДЛЕРЫ
// —————————————————————————————————————————————
func (s Server) GetMedia(ctx context.Context, req GetMediaRequestObject) (GetMediaResponseObject, error) {
id, err := parseInt32(req.Params.ImageId)
if err != nil {
return GetMedia200JSONResponse{Success: ptr(false), Error: ptr("invalid image_id")}, nil
}
img, err := s.db.GetImageByID(ctx, id)
if err != nil {
if err == pgx.ErrNoRows {
return GetMedia200JSONResponse{Success: ptr(false), Error: ptr("image not found")}, nil
}
return nil, err
}
return GetMedia200JSONResponse{
Success: ptr(true),
ImagePath: ptr(img.ImagePath),
}, nil
}
func (s Server) PostMedia(ctx context.Context, req PostMediaRequestObject) (PostMediaResponseObject, error) {
// ❗ Не реализовано: OpenAPI не определяет тело запроса для загрузки
return PostMedia200JSONResponse{
Success: ptr(false),
Error: ptr("upload not implemented: request body not defined in spec"),
}, nil
}
func (s Server) GetUsers(ctx context.Context, req GetUsersRequestObject) (GetUsersResponseObject, error) {
users, err := s.db.ListUsers(ctx, db.ListUsersParams{})
if err != nil {
return nil, err
}
var resp []User
for _, u := range users {
resp = append(resp, mapUser(u))
}
return GetUsers200JSONResponse(resp), nil
}
func (s Server) PostUsers(ctx context.Context, req PostUsersRequestObject) (PostUsersResponseObject, error) {
if req.Body == nil {
return PostUsers200JSONResponse{
Success: ptr(false),
Error: ptr("request body is required"),
}, nil
}
body := *req.Body
// Обязательные поля
nickname, ok := body["nickname"].(string)
if !ok || nickname == "" {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("nickname is required")}, nil
}
mail, ok := body["mail"].(string)
if !ok || mail == "" {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("mail is required")}, nil
}
password, ok := body["password"].(string)
if !ok || password == "" {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("password is required")}, nil
}
// Опциональные поля
var avatarID *int32
if v, ok := body["avatar_id"].(float64); ok {
id := int32(v)
avatarID = &id
}
dispName, _ := body["disp_name"].(string)
userDesc, _ := body["user_desc"].(string)
// 🔐 Хешируем пароль
passhashBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("failed to hash password")}, nil
}
passhash := string(passhashBytes)
// Сохраняем в БД
_, err = s.db.CreateUser(ctx, db.CreateUserParams{
AvatarID: pgtype.Int4{
Int32: 0,
Valid: avatarID != nil,
},
Passhash: passhash,
Mail: pgtype.Text{
String: mail,
Valid: true,
},
Nickname: nickname,
DispName: pgtype.Text{
String: dispName,
Valid: dispName != "",
},
UserDesc: pgtype.Text{
String: userDesc,
Valid: userDesc != "",
},
CreationDate: pgtype.Timestamp{
Time: time.Now(),
Valid: true,
},
})
if err != nil {
// Проверяем нарушение уникальности (например, дубль mail или nickname)
if err.Error() == "ERROR: duplicate key value violates unique constraint \"users_mail_key\"" ||
err.Error() == "ERROR: duplicate key value violates unique constraint \"users_nickname_key\"" {
return PostUsers200JSONResponse{
Success: ptr(false),
Error: ptr("user with this email or nickname already exists"),
}, nil
}
return PostUsers200JSONResponse{Success: ptr(false), Error: ptr("database error")}, nil
}
// Получаем созданного пользователя (без passhash и mail!)
// Предположим, что у вас есть запрос GetUserByNickname или аналогичный
// Но проще — вернуть только ID и nickname
// ⚠️ Поскольку мы не знаем user_id, можно:
// а) добавить RETURNING в CreateUser (рекомендуется),
// б) сделать отдельный SELECT.
// Пока вернём минимальный ответ
userResp := User{
"nickname": nickname,
// "user_id" можно добавить, если обновите query.sql
}
return PostUsers200JSONResponse{
Success: ptr(true),
UserJson: &userResp,
}, nil
}
func (s Server) DeleteUsersUserId(ctx context.Context, req DeleteUsersUserIdRequestObject) (DeleteUsersUserIdResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return DeleteUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
err = s.db.DeleteUser(ctx, userID)
if err != nil {
if err == pgx.ErrNoRows {
return DeleteUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("user not found")}, nil
}
return nil, err
}
return DeleteUsersUserId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetUsersUserId(ctx context.Context, req GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return GetUsersUserId404Response{}, nil
}
user, err := s.db.GetUserByID(ctx, userID)
if err != nil {
if err == pgx.ErrNoRows {
return GetUsersUserId404Response{}, nil
}
return nil, err
}
return GetUsersUserId200JSONResponse(mapUser(user)), nil
}
func (s Server) PatchUsersUserId(ctx context.Context, req PatchUsersUserIdRequestObject) (PatchUsersUserIdResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return PatchUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
if req.Body == nil {
return PatchUsersUserId200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
args := db.UpdateUserParams{
UserID: userID,
}
if v, ok := body["avatar_id"].(float64); ok {
args.AvatarID = pgtype.Int4{Int32: int32(v), Valid: true}
// args.AvatarIDValid = true
}
if v, ok := body["disp_name"].(string); ok {
args.DispName = pgtype.Text{String: v, Valid: true}
// args.DispNameValid = true
}
if v, ok := body["user_desc"].(string); ok {
args.UserDesc = pgtype.Text{String: v, Valid: true}
// args.UserDescValid = true
}
_, err = s.db.UpdateUser(ctx, args)
if err != nil {
return PatchUsersUserId200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PatchUsersUserId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetUsersUserIdReviews(ctx context.Context, req GetUsersUserIdReviewsRequestObject) (GetUsersUserIdReviewsResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return GetUsersUserIdReviews200JSONResponse{}, nil
}
limit := int32(20)
offset := int32(0)
// if req.Params.Limit != nil {
// limit = int32(*req.Params.Limit)
// }
// if req.Params.Offset != nil {
// offset = int32(*req.Params.Offset)
// }
reviews, err := s.db.ListReviewsByUser(ctx, db.ListReviewsByUserParams{
UserID: userID,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
var resp []Review
for _, r := range reviews {
resp = append(resp, mapReview(r))
}
return GetUsersUserIdReviews200JSONResponse(resp), nil
}
func (s Server) DeleteUsersUserIdTitles(ctx context.Context, req DeleteUsersUserIdTitlesRequestObject) (DeleteUsersUserIdTitlesResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return DeleteUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
if req.Params.TitleId != nil {
titleID, err := parseInt32(*req.Params.TitleId)
if err != nil {
return DeleteUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid title_id")}, nil
}
err = s.db.DeleteUserTitle(ctx, db.DeleteUserTitleParams{
UserID: userID,
Column2: titleID,
})
if err != nil && err != pgx.ErrNoRows {
return nil, err
}
}
// else {
// err = s.db.DeleteAllUserTitles(ctx, userID)
// if err != nil {
// return nil, err
// }
// }
return DeleteUsersUserIdTitles200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetUsersUserIdTitles(ctx context.Context, req GetUsersUserIdTitlesRequestObject) (GetUsersUserIdTitlesResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return GetUsersUserIdTitles200JSONResponse{}, nil
}
limit := int32(100)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
titles, err := s.db.ListUserTitles(ctx, db.ListUserTitlesParams{
UserID: userID,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
var resp []UserTitle
for _, t := range titles {
resp = append(resp, mapUserTitle(t))
}
return GetUsersUserIdTitles200JSONResponse(resp), nil
}
func (s Server) PatchUsersUserIdTitles(ctx context.Context, req PatchUsersUserIdTitlesRequestObject) (PatchUsersUserIdTitlesResponseObject, error) {
// userID, err := parseInt32(req.UserId)
// if err != nil {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
// }
// if req.Body == nil {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
// }
// body := *req.Body
// titleID, ok := body["title_id"].(float64)
// if !ok {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("title_id required")}, nil
// }
// args := db.UpdateUserTitleParams{
// UserID: userID,
// TitleID: int32(titleID),
// }
// if v, ok := body["status"].(string); ok {
// args.Status = db.UsertitleStatusT(v)
// // args.StatusValid = true
// }
// if v, ok := body["rate"].(float64); ok {
// args.Rate = pgtype.Int4{Int32: int32(v), Valid: true}
// // args.RateValid = true
// }
// if v, ok := body["review_id"].(float64); ok {
// args.ReviewID = pgtype.Int4{Int32: int32(v), Valid: true}
// // args.ReviewIDValid = true
// }
// _, err = s.db.UpdateUserTitle(ctx, args)
// if err != nil {
// return PatchUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
// }
return PatchUsersUserIdTitles200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) PostUsersUserIdTitles(ctx context.Context, req PostUsersUserIdTitlesRequestObject) (PostUsersUserIdTitlesResponseObject, error) {
userID, err := parseInt32(req.UserId)
if err != nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid user_id")}, nil
}
if req.Body == nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := req.Body
titleID, err := parseInt32(*body.TitleId)
if err != nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr("invalid title_id")}, nil
}
status := db.UsertitleStatusT("planned")
if body.Status != nil {
status = db.UsertitleStatusT(*body.Status)
}
_, err = s.db.CreateUserTitle(ctx, db.CreateUserTitleParams{
UserID: userID,
TitleID: titleID,
Status: status,
Rate: pgtype.Int4{Valid: false},
ReviewID: pgtype.Int4{Valid: false},
})
if err != nil {
return PostUsersUserIdTitles200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PostUsersUserIdTitles200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetTags(ctx context.Context, req GetTagsRequestObject) (GetTagsResponseObject, error) {
limit := int32(100)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
tags, err := s.db.ListTags(ctx, db.ListTagsParams{Limit: limit, Offset: offset})
if err != nil {
return nil, err
}
var resp []Tag
for _, t := range tags {
resp = append(resp, Tag{
"tag_id": t.TagID,
"tag_names": jsonbToInterface(t.TagNames),
})
}
return GetTags200JSONResponse(resp), nil
}
func (s Server) GetTitle(ctx context.Context, req GetTitleRequestObject) (GetTitleResponseObject, error) {
limit := int32(50)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
titles, err := s.db.ListTitles(ctx, db.ListTitlesParams{Limit: limit, Offset: offset})
if err != nil {
return nil, err
}
var resp []Title
for _, t := range titles {
resp = append(resp, mapTitle(t))
}
return GetTitle200JSONResponse(resp), nil
}
func (s Server) GetTitleTitleId(ctx context.Context, req GetTitleTitleIdRequestObject) (GetTitleTitleIdResponseObject, error) {
titleID, err := parseInt32(req.TitleId)
if err != nil {
return GetTitleTitleId404Response{}, nil
}
title, err := s.db.GetTitleByID(ctx, titleID)
if err != nil {
if err == pgx.ErrNoRows {
return GetTitleTitleId404Response{}, nil
}
return nil, err
}
return GetTitleTitleId200JSONResponse(mapTitle(title)), nil
}
func (s Server) PatchTitleTitleId(ctx context.Context, req PatchTitleTitleIdRequestObject) (PatchTitleTitleIdResponseObject, error) {
titleID, err := parseInt32(req.TitleId)
if err != nil {
return PatchTitleTitleId200JSONResponse{Success: ptr(false), Error: ptr("invalid title_id")}, nil
}
if req.Body == nil {
return PatchTitleTitleId200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
args := db.UpdateTitleParams{
TitleID: titleID,
}
if v, ok := body["title_names"].(map[string]interface{}); ok {
data, _ := json.Marshal(v)
args.TitleNames = data
// args.TitleNamesValid = true
}
if v, ok := body["studio_id"].(float64); ok {
args.StudioID = pgtype.Int4{Int32: int32(v), Valid: true}
// args.StudioIDValid = true
}
if v, ok := body["poster_id"].(float64); ok {
args.PosterID = pgtype.Int4{Int32: int32(v), Valid: true}
// args.PosterIDValid = true
}
// if v, ok := body["title_status"].(string); ok {
// args.TitleStatus = db.NullTitleStatusT(v)
// // args.TitleStatusValid = true
// }
if v, ok := body["release_year"].(float64); ok {
args.ReleaseYear = pgtype.Int4{Int32: int32(v), Valid: true}
// args.ReleaseYearValid = true
}
if v, ok := body["episodes_aired"].(float64); ok {
args.EpisodesAired = pgtype.Int4{Int32: int32(v), Valid: true}
// args.EpisodesAiredValid = true
}
if v, ok := body["episodes_all"].(float64); ok {
args.EpisodesAll = pgtype.Int4{Int32: int32(v), Valid: true}
// args.EpisodesAllValid = true
}
_, err = s.db.UpdateTitle(ctx, args)
if err != nil {
return PatchTitleTitleId200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PatchTitleTitleId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) GetTitleTitleIdReviews(ctx context.Context, req GetTitleTitleIdReviewsRequestObject) (GetTitleTitleIdReviewsResponseObject, error) {
titleID, err := parseInt32(req.TitleId)
if err != nil {
return GetTitleTitleIdReviews200JSONResponse{}, nil
}
limit := int32(20)
offset := int32(0)
if req.Params.Limit != nil {
limit = int32(*req.Params.Limit)
}
if req.Params.Offset != nil {
offset = int32(*req.Params.Offset)
}
reviews, err := s.db.ListReviewsByTitle(ctx, db.ListReviewsByTitleParams{
TitleID: titleID,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
var resp []Review
for _, r := range reviews {
resp = append(resp, mapReview(r))
}
return GetTitleTitleIdReviews200JSONResponse(resp), nil
}
func (s Server) PostReviews(ctx context.Context, req PostReviewsRequestObject) (PostReviewsResponseObject, error) {
if req.Body == nil {
return PostReviews200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
userID, ok1 := body["user_id"].(float64)
titleID, ok2 := body["title_id"].(float64)
reviewText, ok3 := body["review_text"].(string)
if !ok1 || !ok2 || !ok3 {
return PostReviews200JSONResponse{Success: ptr(false), Error: ptr("user_id, title_id, review_text required")}, nil
}
var imageIDs []int32
if ids, ok := body["image_ids"].([]interface{}); ok {
for _, id := range ids {
if f, ok := id.(float64); ok {
imageIDs = append(imageIDs, int32(f))
}
}
}
_, err := s.db.CreateReview(ctx, db.CreateReviewParams{
UserID: int32(userID),
TitleID: int32(titleID),
ImageIds: imageIDs,
ReviewText: reviewText,
CreationDate: pgtype.Timestamp{Time: time.Now(), Valid: true},
})
if err != nil {
return PostReviews200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PostReviews200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) DeleteReviewsReviewId(ctx context.Context, req DeleteReviewsReviewIdRequestObject) (DeleteReviewsReviewIdResponseObject, error) {
reviewID, err := parseInt32(req.ReviewId)
if err != nil {
return DeleteReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("invalid review_id")}, nil
}
err = s.db.DeleteReview(ctx, reviewID)
if err != nil {
if err == pgx.ErrNoRows {
return DeleteReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("review not found")}, nil
}
return nil, err
}
return DeleteReviewsReviewId200JSONResponse{Success: ptr(true)}, nil
}
func (s Server) PatchReviewsReviewId(ctx context.Context, req PatchReviewsReviewIdRequestObject) (PatchReviewsReviewIdResponseObject, error) {
reviewID, err := parseInt32(req.ReviewId)
if err != nil {
return PatchReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("invalid review_id")}, nil
}
if req.Body == nil {
return PatchReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr("empty body")}, nil
}
body := *req.Body
args := db.UpdateReviewParams{
ReviewID: reviewID,
}
if v, ok := body["review_text"].(string); ok {
args.ReviewText = pgtype.Text{String: v, Valid: true}
// args.ReviewTextValid = true
}
if ids, ok := body["image_ids"].([]interface{}); ok {
var imageIDs []int32
for _, id := range ids {
if f, ok := id.(float64); ok {
imageIDs = append(imageIDs, int32(f))
}
}
args.ImageIds = imageIDs
// args.ImageIdsValid = true
}
_, err = s.db.UpdateReview(ctx, args)
if err != nil {
return PatchReviewsReviewId200JSONResponse{Success: ptr(false), Error: ptr(err.Error())}, nil
}
return PatchReviewsReviewId200JSONResponse{Success: ptr(true)}, nil
}
// —————————————————————————————————————————————
// МАППИНГИ
// —————————————————————————————————————————————
func mapUser(u db.Users) User {
return User{
"user_id": u.UserID,
"avatar_id": pgInt4ToPtr(u.AvatarID),
"nickname": u.Nickname,
"disp_name": pgTextToPtr(u.DispName),
"user_desc": pgTextToPtr(u.UserDesc),
"creation_date": u.CreationDate.Time,
// mail и passhash НЕ возвращаем!
}
}
func mapTitle(t db.Titles) Title {
var releaseSeason interface{}
if t.ReleaseSeason.Valid {
releaseSeason = string(t.ReleaseSeason.ReleaseSeasonT)
}
return Title{
"title_id": t.TitleID,
"title_names": jsonbToInterface(t.TitleNames),
"studio_id": t.StudioID,
"poster_id": pgInt4ToPtr(t.PosterID),
"signal_ids": t.SignalIds,
"title_status": string(t.TitleStatus),
"rating": pgFloat8ToPtr(t.Rating),
"rating_count": pgInt4ToPtr(t.RatingCount),
"release_year": pgInt4ToPtr(t.ReleaseYear),
"release_season": releaseSeason,
"season": pgInt4ToPtr(t.Season),
"episodes_aired": pgInt4ToPtr(t.EpisodesAired),
"episodes_all": pgInt4ToPtr(t.EpisodesAll),
"episodes_len": jsonbToInterface(t.EpisodesLen),
}
}
func mapReview(r db.Reviews) Review {
return Review{
"review_id": r.ReviewID,
"user_id": r.UserID,
"title_id": r.TitleID,
"image_ids": r.ImageIds,
"review_text": r.ReviewText,
"creation_date": r.CreationDate.Time,
}
}
func mapUserTitle(ut db.Usertitles) UserTitle {
return UserTitle{
"usertitle_id": ut.UsertitleID,
"user_id": ut.UserID,
"title_id": ut.TitleID,
"status": string(ut.Status),
"rate": pgInt4ToPtr(ut.Rate),
"review_id": pgInt4ToPtr(ut.ReviewID),
}
}

View file

@ -1,712 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: query.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createImage = `-- name: CreateImage :one
INSERT INTO images (storage_type, image_path)
VALUES ($1, $2)
RETURNING image_id, storage_type, image_path
`
type CreateImageParams struct {
StorageType StorageTypeT `db:"storage_type" json:"storage_type"`
ImagePath string `db:"image_path" json:"image_path"`
}
func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Images, error) {
row := q.db.QueryRow(ctx, createImage, arg.StorageType, arg.ImagePath)
var i Images
err := row.Scan(&i.ImageID, &i.StorageType, &i.ImagePath)
return i, err
}
const createReview = `-- name: CreateReview :one
INSERT INTO reviews (user_id, title_id, image_ids, review_text, creation_date)
VALUES ($1, $2, $3, $4, $5)
RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date
`
type CreateReviewParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
ImageIds []int32 `db:"image_ids" json:"image_ids"`
ReviewText string `db:"review_text" json:"review_text"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
func (q *Queries) CreateReview(ctx context.Context, arg CreateReviewParams) (Reviews, error) {
row := q.db.QueryRow(ctx, createReview,
arg.UserID,
arg.TitleID,
arg.ImageIds,
arg.ReviewText,
arg.CreationDate,
)
var i Reviews
err := row.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
)
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date
`
type CreateUserParams struct {
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Passhash string `db:"passhash" json:"passhash"`
Mail pgtype.Text `db:"mail" json:"mail"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
type CreateUserRow struct {
UserID int32 `db:"user_id" json:"user_id"`
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.db.QueryRow(ctx, createUser,
arg.AvatarID,
arg.Passhash,
arg.Mail,
arg.Nickname,
arg.DispName,
arg.UserDesc,
arg.CreationDate,
)
var i CreateUserRow
err := row.Scan(
&i.UserID,
&i.AvatarID,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
)
return i, err
}
const createUserTitle = `-- name: CreateUserTitle :one
INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING usertitle_id, user_id, title_id, status, rate, review_id
`
type CreateUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
Status UsertitleStatusT `db:"status" json:"status"`
Rate pgtype.Int4 `db:"rate" json:"rate"`
ReviewID pgtype.Int4 `db:"review_id" json:"review_id"`
}
func (q *Queries) CreateUserTitle(ctx context.Context, arg CreateUserTitleParams) (Usertitles, error) {
row := q.db.QueryRow(ctx, createUserTitle,
arg.UserID,
arg.TitleID,
arg.Status,
arg.Rate,
arg.ReviewID,
)
var i Usertitles
err := row.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
)
return i, err
}
const deleteReview = `-- name: DeleteReview :exec
DELETE FROM reviews
WHERE review_id = $1
`
func (q *Queries) DeleteReview(ctx context.Context, reviewID int32) error {
_, err := q.db.Exec(ctx, deleteReview, reviewID)
return err
}
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users
WHERE user_id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, userID int32) error {
_, err := q.db.Exec(ctx, deleteUser, userID)
return err
}
const deleteUserTitle = `-- name: DeleteUserTitle :exec
DELETE FROM usertitles
WHERE user_id = $1 AND ($2::int IS NULL OR title_id = $2)
`
type DeleteUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
Column2 int32 `db:"column_2" json:"column_2"`
}
func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams) error {
_, err := q.db.Exec(ctx, deleteUserTitle, arg.UserID, arg.Column2)
return err
}
const getImageByID = `-- name: GetImageByID :one
SELECT image_id, storage_type, image_path
FROM images
WHERE image_id = $1
`
func (q *Queries) GetImageByID(ctx context.Context, imageID int32) (Images, error) {
row := q.db.QueryRow(ctx, getImageByID, imageID)
var i Images
err := row.Scan(&i.ImageID, &i.StorageType, &i.ImagePath)
return i, err
}
const getReviewByID = `-- name: GetReviewByID :one
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE review_id = $1
`
func (q *Queries) GetReviewByID(ctx context.Context, reviewID int32) (Reviews, error) {
row := q.db.QueryRow(ctx, getReviewByID, reviewID)
var i Reviews
err := row.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
)
return i, err
}
const getTitleByID = `-- name: GetTitleByID :one
SELECT title_id, title_names, studio_id, poster_id, signal_ids,
title_status, rating, rating_count, release_year, release_season,
season, episodes_aired, episodes_all, episodes_len
FROM titles
WHERE title_id = $1
`
func (q *Queries) GetTitleByID(ctx context.Context, titleID int32) (Titles, error) {
row := q.db.QueryRow(ctx, getTitleByID, titleID)
var i Titles
err := row.Scan(
&i.TitleID,
&i.TitleNames,
&i.StudioID,
&i.PosterID,
&i.SignalIds,
&i.TitleStatus,
&i.Rating,
&i.RatingCount,
&i.ReleaseYear,
&i.ReleaseSeason,
&i.Season,
&i.EpisodesAired,
&i.EpisodesAll,
&i.EpisodesLen,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
FROM users
WHERE user_id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, userID int32) (Users, error) {
row := q.db.QueryRow(ctx, getUserByID, userID)
var i Users
err := row.Scan(
&i.UserID,
&i.AvatarID,
&i.Passhash,
&i.Mail,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
)
return i, err
}
const getUserTitle = `-- name: GetUserTitle :one
SELECT usertitle_id, user_id, title_id, status, rate, review_id
FROM usertitles
WHERE user_id = $1 AND title_id = $2
`
type GetUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
}
func (q *Queries) GetUserTitle(ctx context.Context, arg GetUserTitleParams) (Usertitles, error) {
row := q.db.QueryRow(ctx, getUserTitle, arg.UserID, arg.TitleID)
var i Usertitles
err := row.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
)
return i, err
}
const listReviewsByTitle = `-- name: ListReviewsByTitle :many
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE title_id = $1
ORDER BY creation_date DESC
LIMIT $2 OFFSET $3
`
type ListReviewsByTitleParams struct {
TitleID int32 `db:"title_id" json:"title_id"`
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListReviewsByTitle(ctx context.Context, arg ListReviewsByTitleParams) ([]Reviews, error) {
rows, err := q.db.Query(ctx, listReviewsByTitle, arg.TitleID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Reviews
for rows.Next() {
var i Reviews
if err := rows.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listReviewsByUser = `-- name: ListReviewsByUser :many
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE user_id = $1
ORDER BY creation_date DESC
LIMIT $2 OFFSET $3
`
type ListReviewsByUserParams struct {
UserID int32 `db:"user_id" json:"user_id"`
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListReviewsByUser(ctx context.Context, arg ListReviewsByUserParams) ([]Reviews, error) {
rows, err := q.db.Query(ctx, listReviewsByUser, arg.UserID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Reviews
for rows.Next() {
var i Reviews
if err := rows.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTags = `-- name: ListTags :many
SELECT tag_id, tag_names
FROM tags
ORDER BY tag_id
LIMIT $1 OFFSET $2
`
type ListTagsParams struct {
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListTags(ctx context.Context, arg ListTagsParams) ([]Tags, error) {
rows, err := q.db.Query(ctx, listTags, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Tags
for rows.Next() {
var i Tags
if err := rows.Scan(&i.TagID, &i.TagNames); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTitles = `-- name: ListTitles :many
SELECT title_id, title_names, studio_id, poster_id, signal_ids,
title_status, rating, rating_count, release_year, release_season,
season, episodes_aired, episodes_all, episodes_len
FROM titles
ORDER BY title_id
LIMIT $1 OFFSET $2
`
type ListTitlesParams struct {
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListTitles(ctx context.Context, arg ListTitlesParams) ([]Titles, error) {
rows, err := q.db.Query(ctx, listTitles, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Titles
for rows.Next() {
var i Titles
if err := rows.Scan(
&i.TitleID,
&i.TitleNames,
&i.StudioID,
&i.PosterID,
&i.SignalIds,
&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
}
const listUserTitles = `-- name: ListUserTitles :many
SELECT usertitle_id, user_id, title_id, status, rate, review_id
FROM usertitles
WHERE user_id = $1
ORDER BY usertitle_id
LIMIT $2 OFFSET $3
`
type ListUserTitlesParams struct {
UserID int32 `db:"user_id" json:"user_id"`
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListUserTitles(ctx context.Context, arg ListUserTitlesParams) ([]Usertitles, error) {
rows, err := q.db.Query(ctx, listUserTitles, arg.UserID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Usertitles
for rows.Next() {
var i Usertitles
if err := rows.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listUsers = `-- name: ListUsers :many
SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
FROM users
ORDER BY user_id
LIMIT $1 OFFSET $2
`
type ListUsersParams struct {
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]Users, error) {
rows, err := q.db.Query(ctx, listUsers, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Users
for rows.Next() {
var i Users
if err := rows.Scan(
&i.UserID,
&i.AvatarID,
&i.Passhash,
&i.Mail,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateReview = `-- name: UpdateReview :one
UPDATE reviews
SET
image_ids = COALESCE($1, image_ids),
review_text = COALESCE($2, review_text)
WHERE review_id = $3
RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date
`
type UpdateReviewParams struct {
ImageIds []int32 `db:"image_ids" json:"image_ids"`
ReviewText pgtype.Text `db:"review_text" json:"review_text"`
ReviewID int32 `db:"review_id" json:"review_id"`
}
func (q *Queries) UpdateReview(ctx context.Context, arg UpdateReviewParams) (Reviews, error) {
row := q.db.QueryRow(ctx, updateReview, arg.ImageIds, arg.ReviewText, arg.ReviewID)
var i Reviews
err := row.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
)
return i, err
}
const updateTitle = `-- name: UpdateTitle :one
UPDATE titles
SET
title_names = COALESCE($1, title_names),
studio_id = COALESCE($2, studio_id),
poster_id = COALESCE($3, poster_id),
signal_ids = COALESCE($4, signal_ids),
title_status = COALESCE($5, title_status),
release_year = COALESCE($6, release_year),
release_season = COALESCE($7, release_season),
episodes_aired = COALESCE($8, episodes_aired),
episodes_all = COALESCE($9, episodes_all),
episodes_len = COALESCE($10, episodes_len)
WHERE title_id = $11
RETURNING title_id, title_names, studio_id, poster_id, signal_ids, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len
`
type UpdateTitleParams struct {
TitleNames []byte `db:"title_names" json:"title_names"`
StudioID pgtype.Int4 `db:"studio_id" json:"studio_id"`
PosterID pgtype.Int4 `db:"poster_id" json:"poster_id"`
SignalIds []int32 `db:"signal_ids" json:"signal_ids"`
TitleStatus NullTitleStatusT `db:"title_status" json:"title_status"`
ReleaseYear pgtype.Int4 `db:"release_year" json:"release_year"`
ReleaseSeason NullReleaseSeasonT `db:"release_season" json:"release_season"`
EpisodesAired pgtype.Int4 `db:"episodes_aired" json:"episodes_aired"`
EpisodesAll pgtype.Int4 `db:"episodes_all" json:"episodes_all"`
EpisodesLen []byte `db:"episodes_len" json:"episodes_len"`
TitleID int32 `db:"title_id" json:"title_id"`
}
func (q *Queries) UpdateTitle(ctx context.Context, arg UpdateTitleParams) (Titles, error) {
row := q.db.QueryRow(ctx, updateTitle,
arg.TitleNames,
arg.StudioID,
arg.PosterID,
arg.SignalIds,
arg.TitleStatus,
arg.ReleaseYear,
arg.ReleaseSeason,
arg.EpisodesAired,
arg.EpisodesAll,
arg.EpisodesLen,
arg.TitleID,
)
var i Titles
err := row.Scan(
&i.TitleID,
&i.TitleNames,
&i.StudioID,
&i.PosterID,
&i.SignalIds,
&i.TitleStatus,
&i.Rating,
&i.RatingCount,
&i.ReleaseYear,
&i.ReleaseSeason,
&i.Season,
&i.EpisodesAired,
&i.EpisodesAll,
&i.EpisodesLen,
)
return i, err
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
avatar_id = COALESCE($1, avatar_id),
disp_name = COALESCE($2, disp_name),
user_desc = COALESCE($3, user_desc)
WHERE user_id = $4
RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date
`
type UpdateUserParams struct {
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
UserID int32 `db:"user_id" json:"user_id"`
}
type UpdateUserRow struct {
UserID int32 `db:"user_id" json:"user_id"`
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) {
row := q.db.QueryRow(ctx, updateUser,
arg.AvatarID,
arg.DispName,
arg.UserDesc,
arg.UserID,
)
var i UpdateUserRow
err := row.Scan(
&i.UserID,
&i.AvatarID,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
)
return i, err
}
const updateUserTitle = `-- name: UpdateUserTitle :one
UPDATE usertitles
SET
status = COALESCE($3, status),
rate = COALESCE($4, rate),
review_id = COALESCE($5, review_id)
WHERE user_id = $1 AND title_id = $2
RETURNING usertitle_id, user_id, title_id, status, rate, review_id
`
type UpdateUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
Status NullUsertitleStatusT `db:"status" json:"status"`
Rate pgtype.Int4 `db:"rate" json:"rate"`
ReviewID pgtype.Int4 `db:"review_id" json:"review_id"`
}
func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams) (Usertitles, error) {
row := q.db.QueryRow(ctx, updateUserTitle,
arg.UserID,
arg.TitleID,
arg.Status,
arg.Rate,
arg.ReviewID,
)
var i Usertitles
err := row.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
)
return i, err
}

View file

@ -0,0 +1,19 @@
package handlers
import (
sqlc "nyanimedb/sql"
"strconv"
)
type Server struct {
db *sqlc.Queries
}
func NewServer(db *sqlc.Queries) Server {
return Server{db: db}
}
func parseInt64(s string) (int32, error) {
i, err := strconv.ParseInt(s, 10, 64)
return int32(i), err
}

View file

@ -0,0 +1,50 @@
package handlers
import (
"context"
oapi "nyanimedb/api"
sqlc "nyanimedb/sql"
"github.com/jackc/pgx/v5"
"github.com/oapi-codegen/runtime/types"
)
// type Server struct {
// db *sqlc.Queries
// }
// func NewServer(db *sqlc.Queries) Server {
// return Server{db: db}
// }
// func parseInt64(s string) (int32, error) {
// i, err := strconv.ParseInt(s, 10, 64)
// return int32(i), err
// }
func mapUser(u sqlc.GetUserByIDRow) oapi.User {
return oapi.User{
AvatarId: u.AvatarID,
CreationDate: u.CreationDate,
DispName: u.DispName,
Id: &u.ID,
Mail: (*types.Email)(u.Mail),
Nickname: u.Nickname,
UserDesc: u.UserDesc,
}
}
func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdRequestObject) (oapi.GetUsersUserIdResponseObject, error) {
userID, err := parseInt64(req.UserId)
if err != nil {
return oapi.GetUsersUserId404Response{}, nil
}
user, err := s.db.GetUserByID(context.TODO(), int64(userID))
if err != nil {
if err == pgx.ErrNoRows {
return oapi.GetUsersUserId404Response{}, nil
}
return nil, err
}
return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil
}

View file

@ -3,12 +3,14 @@ package main
import (
"context"
"fmt"
"nyanimedb-server/api"
"nyanimedb-server/db"
sqlc "nyanimedb/sql"
"os"
"reflect"
"time"
oapi "nyanimedb/api"
handlers "nyanimedb/modules/backend/handlers"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
@ -39,9 +41,9 @@ func main() {
r := gin.Default()
queries := db.New(conn)
queries := sqlc.New(conn)
server := api.NewServer(queries)
server := handlers.NewServer(queries)
// r.LoadHTMLGlob("templates/*")
r.Use(cors.New(cors.Config{
@ -53,10 +55,10 @@ func main() {
MaxAge: 12 * time.Hour,
}))
api.RegisterHandlers(r, api.NewStrictHandler(
oapi.RegisterHandlers(r, oapi.NewStrictHandler(
server,
// сюда можно добавить middlewares, если нужно
[]api.StrictMiddlewareFunc{},
[]oapi.StrictMiddlewareFunc{},
))
// r.GET("/", func(c *gin.Context) {
// c.HTML(http.StatusOK, "index.html", gin.H{

142
modules/backend/queries.sql Normal file
View file

@ -0,0 +1,142 @@
-- name: GetImageByID :one
SELECT id, storage_type, image_path
FROM images
WHERE id = $1;
-- name: CreateImage :one
INSERT INTO images (storage_type, image_path)
VALUES ($1, $2)
RETURNING id, storage_type, image_path;
-- name: GetUserByID :one
SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date
FROM users
WHERE id = $1;
-- -- name: ListUsers :many
-- SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
-- FROM users
-- ORDER BY user_id
-- LIMIT $1 OFFSET $2;
-- -- name: CreateUser :one
-- INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date)
-- VALUES ($1, $2, $3, $4, $5, $6, $7)
-- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
-- -- name: UpdateUser :one
-- UPDATE users
-- SET
-- avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id),
-- disp_name = COALESCE(sqlc.narg('disp_name'), disp_name),
-- user_desc = COALESCE(sqlc.narg('user_desc'), user_desc),
-- passhash = COALESCE(sqlc.narg('passhash'), passhash)
-- WHERE user_id = sqlc.arg('user_id')
-- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
-- -- name: DeleteUser :exec
-- DELETE FROM users
-- WHERE user_id = $1;
-- -- name: GetTitleByID :one
-- SELECT title_id, title_names, studio_id, poster_id, signal_ids,
-- title_status, rating, rating_count, release_year, release_season,
-- season, episodes_aired, episodes_all, episodes_len
-- FROM titles
-- WHERE title_id = $1;
-- -- name: ListTitles :many
-- SELECT title_id, title_names, studio_id, poster_id, signal_ids,
-- title_status, rating, rating_count, release_year, release_season,
-- season, episodes_aired, episodes_all, episodes_len
-- FROM titles
-- ORDER BY title_id
-- LIMIT $1 OFFSET $2;
-- -- name: UpdateTitle :one
-- UPDATE titles
-- SET
-- title_names = COALESCE(sqlc.narg('title_names'), title_names),
-- studio_id = COALESCE(sqlc.narg('studio_id'), studio_id),
-- poster_id = COALESCE(sqlc.narg('poster_id'), poster_id),
-- signal_ids = COALESCE(sqlc.narg('signal_ids'), signal_ids),
-- title_status = COALESCE(sqlc.narg('title_status'), title_status),
-- release_year = COALESCE(sqlc.narg('release_year'), release_year),
-- release_season = COALESCE(sqlc.narg('release_season'), release_season),
-- episodes_aired = COALESCE(sqlc.narg('episodes_aired'), episodes_aired),
-- episodes_all = COALESCE(sqlc.narg('episodes_all'), episodes_all),
-- episodes_len = COALESCE(sqlc.narg('episodes_len'), episodes_len)
-- WHERE title_id = sqlc.arg('title_id')
-- RETURNING *;
-- -- name: GetReviewByID :one
-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
-- FROM reviews
-- WHERE review_id = $1;
-- -- name: CreateReview :one
-- INSERT INTO reviews (user_id, title_id, image_ids, review_text, creation_date)
-- VALUES ($1, $2, $3, $4, $5)
-- RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date;
-- -- name: UpdateReview :one
-- UPDATE reviews
-- SET
-- image_ids = COALESCE(sqlc.narg('image_ids'), image_ids),
-- review_text = COALESCE(sqlc.narg('review_text'), review_text)
-- WHERE review_id = sqlc.arg('review_id')
-- RETURNING *;
-- -- name: DeleteReview :exec
-- DELETE FROM reviews
-- WHERE review_id = $1;
-- -- name: ListReviewsByTitle :many
-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
-- FROM reviews
-- WHERE title_id = $1
-- ORDER BY creation_date DESC
-- LIMIT $2 OFFSET $3;
-- -- name: ListReviewsByUser :many
-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
-- FROM reviews
-- WHERE user_id = $1
-- ORDER BY creation_date DESC
-- LIMIT $2 OFFSET $3;
-- -- name: GetUserTitle :one
-- SELECT usertitle_id, user_id, title_id, status, rate, review_id
-- FROM usertitles
-- WHERE user_id = $1 AND title_id = $2;
-- -- name: ListUserTitles :many
-- SELECT usertitle_id, user_id, title_id, status, rate, review_id
-- FROM usertitles
-- WHERE user_id = $1
-- ORDER BY usertitle_id
-- LIMIT $2 OFFSET $3;
-- -- name: CreateUserTitle :one
-- INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
-- VALUES ($1, $2, $3, $4, $5)
-- RETURNING usertitle_id, user_id, title_id, status, rate, review_id;
-- -- name: UpdateUserTitle :one
-- UPDATE usertitles
-- SET
-- status = COALESCE(sqlc.narg('status'), status),
-- rate = COALESCE(sqlc.narg('rate'), rate),
-- review_id = COALESCE(sqlc.narg('review_id'), review_id)
-- WHERE user_id = $1 AND title_id = $2
-- RETURNING *;
-- -- name: DeleteUserTitle :exec
-- DELETE FROM usertitles
-- WHERE user_id = $1 AND ($2::int IS NULL OR title_id = $2);
-- -- name: ListTags :many
-- SELECT tag_id, tag_names
-- FROM tags
-- ORDER BY tag_id
-- LIMIT $1 OFFSET $2;

View file

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .title }}</title>
</head>
<body>
<h1>{{ .message }}</h1>
</body>
</html>

View file

@ -0,0 +1,29 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/v1/ {
rewrite ^/api/v1/(.*)$ /$1 break;
proxy_pass http://nyanimedb-backend:8080/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
#error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View file

@ -10,7 +10,8 @@
"dependencies": {
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.4"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@ -22,11 +23,30 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"openapi-typescript-codegen": "^0.29.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.9.3",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz",
"integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@ -1011,6 +1031,13 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true,
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1928,6 +1955,19 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001749",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
@ -1998,6 +2038,16 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2012,6 +2062,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2539,6 +2598,21 @@
"node": ">= 6"
}
},
"node_modules/fs-extra": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2648,6 +2722,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@ -2655,6 +2736,28 @@
"dev": true,
"license": "MIT"
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -2848,6 +2951,19 @@
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -2972,6 +3088,16 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3005,6 +3131,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.23",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
@ -3012,6 +3145,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/openapi-typescript-codegen": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz",
"integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.5.4",
"camelcase": "^6.3.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8"
},
"bin": {
"openapi": "bin/index.js"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -3206,6 +3356,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -3223,6 +3374,44 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.9.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.9.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
"integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
"license": "MIT",
"dependencies": {
"react-router": "7.9.4"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -3326,6 +3515,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3349,6 +3544,16 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -3512,6 +3717,20 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undici-types": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
@ -3519,6 +3738,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@ -3694,6 +3923,13 @@
"node": ">=0.10.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View file

@ -12,7 +12,8 @@
"dependencies": {
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.4"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@ -24,6 +25,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"openapi-typescript-codegen": "^0.29.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"

View file

@ -1,38 +1,15 @@
import React, { useEffect, useState } from "react";
import { fetchItems } from "./services/api";
import type { Item } from "./services/api";
import ItemTemplate from "./components/ItemTemplate";
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UserPage from "./components/UserPage/UserPage";
const App: React.FC = () => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const getData = async () => {
try {
const data = await fetchItems();
setItems(data);
} catch (err) {
setError("Failed to fetch items.");
} finally {
setLoading(false);
}
};
getData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>{error}</div>;
return (
<div style={{ padding: "2rem" }}>
<h1>Items List</h1>
{items.map((item) => (
<ItemTemplate key={item.id} item={item} />
))}
</div>
<Router>
<Routes>
<Route path="/users/:id" element={<UserPage />} />
</Routes>
</Router>
);
};
export default App;
export default App;

View file

@ -0,0 +1,25 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

View file

@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};

View file

@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};

View file

@ -0,0 +1,131 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
if (this.#resolve) this.#resolve(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
if (this.#reject) this.#reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
if (this.#reject) this.#reject(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}

View file

@ -0,0 +1,32 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: '/api/v1',
VERSION: '1.0.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};

View file

@ -0,0 +1,323 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import axios from 'axios';
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
import FormData from 'form-data';
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === 'string';
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== '';
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === 'object' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
typeof value.arrayBuffer === 'function' &&
typeof value.constructor === 'function' &&
typeof value.constructor.name === 'string' &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};
export const isSuccess = (status: number): boolean => {
return status >= 200 && status < 300;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach(v => {
process(key, v);
});
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
}
return '';
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
const [token, username, password, additionalHeaders] = await Promise.all([
resolve(options, config.TOKEN),
resolve(options, config.USERNAME),
resolve(options, config.PASSWORD),
resolve(options, config.HEADERS),
]);
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
...formHeaders,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return headers;
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body) {
return options.body;
}
return undefined;
};
export const sendRequest = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Record<string, string>,
onCancel: OnCancel,
axiosClient: AxiosInstance
): Promise<AxiosResponse<T>> => {
const source = axios.CancelToken.source();
const requestConfig: AxiosRequestConfig = {
url,
headers,
data: body ?? formData,
method: options.method,
withCredentials: config.WITH_CREDENTIALS,
withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false,
cancelToken: source.token,
};
onCancel(() => source.cancel('The user aborted a request.'));
try {
return await axiosClient.request(requestConfig);
} catch (error) {
const axiosError = error as AxiosError<T>;
if (axiosError.response) {
return axiosError.response;
}
throw error;
}
};
export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers[responseHeader];
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = (response: AxiosResponse<any>): any => {
if (response.status !== 204) {
return response.data;
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
...options.errors,
}
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @param axiosClient The axios client instance to use
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options, formData);
if (!onCancel.isCancelled) {
const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

View file

@ -0,0 +1,16 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export { ApiError } from './core/ApiError';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export type { Review } from './models/Review';
export type { Tag } from './models/Tag';
export type { Title } from './models/Title';
export type { User } from './models/User';
export type { UserTitle } from './models/UserTitle';
export { DefaultService } from './services/DefaultService';

View file

@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Review = Record<string, any>;

View file

@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Tag = Record<string, any>;

View file

@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Title = Record<string, any>;

View file

@ -0,0 +1,35 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type User = {
/**
* Unique user ID (primary key)
*/
id?: number;
/**
* ID of the user avatar (references images table)
*/
avatar_id?: number | null;
/**
* User email
*/
mail?: string;
/**
* Username (alphanumeric + _ or -)
*/
nickname: string;
/**
* Display name
*/
disp_name?: string;
/**
* User description
*/
user_desc?: string;
/**
* Timestamp when the user was created
*/
creation_date: string;
};

View file

@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UserTitle = Record<string, any>;

View file

@ -0,0 +1,35 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { User } from '../models/User';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class DefaultService {
/**
* Get user info
* @param userId
* @param fields
* @returns User User info
* @throws ApiError
*/
public static getUsers(
userId: string,
fields: string = 'all',
): CancelablePromise<User> {
return __request(OpenAPI, {
method: 'GET',
url: '/users/{user_id}',
path: {
'user_id': userId,
},
query: {
'fields': fields,
},
errors: {
404: `User not found`,
},
});
}
}

View file

@ -0,0 +1,97 @@
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 3rem 1rem;
background-color: #f5f6fa;
min-height: 100vh;
font-family: "Inter", sans-serif;
}
.card {
background-color: #ffffff;
border-radius: 1rem;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
padding: 2rem;
max-width: 400px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar {
margin-bottom: 1.5rem;
}
.avatarImg {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #4a90e2;
}
.avatarPlaceholder {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: #dcdde1;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: #4a4a4a;
font-weight: bold;
border: 3px solid #4a90e2;
}
.info {
display: flex;
flex-direction: column;
align-items: center;
}
.name {
font-size: 1.8rem;
font-weight: 700;
margin: 0.25rem 0;
color: #2f3640;
}
.nickname {
font-size: 1rem;
color: #718093;
margin-bottom: 1rem;
}
.desc {
font-size: 1rem;
color: #353b48;
margin-bottom: 1rem;
}
.created {
font-size: 0.9rem;
color: #7f8fa6;
}
.loader {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
font-size: 1.5rem;
color: #4a90e2;
}
.error {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
color: #e84118;
font-weight: 600;
font-size: 1.2rem;
}

View file

@ -0,0 +1,64 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; // <-- import
import { DefaultService } from "../../api/services/DefaultService";
import type { User } from "../../api/models/User";
import styles from "./UserPage.module.css";
const UserPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); // <-- get user id from URL
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!id) return;
const getUserInfo = async () => {
try {
const userInfo = await DefaultService.getUsers(id, "all"); // <-- use dynamic id
setUser(userInfo);
} catch (err) {
console.error(err);
setError("Failed to fetch user info.");
} finally {
setLoading(false);
}
};
getUserInfo();
}, [id]);
if (loading) return <div className={styles.loader}>Loading...</div>;
if (error) return <div className={styles.error}>{error}</div>;
if (!user) return <div className={styles.error}>User not found.</div>;
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.avatar}>
{user.avatar_id ? (
<img
src={`/images/${user.avatar_id}.png`}
alt="User Avatar"
className={styles.avatarImg}
/>
) : (
<div className={styles.avatarPlaceholder}>
{user.disp_name?.[0] || "U"}
</div>
)}
</div>
<div className={styles.info}>
<h1 className={styles.name}>{user.disp_name || user.nickname}</h1>
<p className={styles.nickname}>@{user.nickname}</p>
{user.user_desc && <p className={styles.desc}>{user.user_desc}</p>}
<p className={styles.created}>
Joined: {new Date(user.creation_date).toLocaleDateString()}
</p>
</div>
</div>
</div>
);
};
export default UserPage;

View file

@ -1,3 +0,0 @@
module tutorial.sqlc.dev/app
go 1.25.2

View file

@ -1,142 +0,0 @@
-- name: GetImageByID :one
SELECT image_id, storage_type, image_path
FROM images
WHERE image_id = $1;
-- name: CreateImage :one
INSERT INTO images (storage_type, image_path)
VALUES ($1, $2)
RETURNING image_id, storage_type, image_path;
-- name: GetUserByID :one
SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
FROM users
WHERE user_id = $1;
-- name: ListUsers :many
SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
FROM users
ORDER BY user_id
LIMIT $1 OFFSET $2;
-- name: CreateUser :one
INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
-- name: UpdateUser :one
UPDATE users
SET
avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id),
disp_name = COALESCE(sqlc.narg('disp_name'), disp_name),
user_desc = COALESCE(sqlc.narg('user_desc'), user_desc),
passhash = COALESCE(sqlc.narg('passhash'), passhash),
WHERE user_id = sqlc.arg('user_id')
RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date;
-- name: DeleteUser :exec
DELETE FROM users
WHERE user_id = $1;
-- name: GetTitleByID :one
SELECT title_id, title_names, studio_id, poster_id, signal_ids,
title_status, rating, rating_count, release_year, release_season,
season, episodes_aired, episodes_all, episodes_len
FROM titles
WHERE title_id = $1;
-- name: ListTitles :many
SELECT title_id, title_names, studio_id, poster_id, signal_ids,
title_status, rating, rating_count, release_year, release_season,
season, episodes_aired, episodes_all, episodes_len
FROM titles
ORDER BY title_id
LIMIT $1 OFFSET $2;
-- name: UpdateTitle :one
UPDATE titles
SET
title_names = COALESCE(sqlc.narg('title_names'), title_names),
studio_id = COALESCE(sqlc.narg('studio_id'), studio_id),
poster_id = COALESCE(sqlc.narg('poster_id'), poster_id),
signal_ids = COALESCE(sqlc.narg('signal_ids'), signal_ids),
title_status = COALESCE(sqlc.narg('title_status'), title_status),
release_year = COALESCE(sqlc.narg('release_year'), release_year),
release_season = COALESCE(sqlc.narg('release_season'), release_season),
episodes_aired = COALESCE(sqlc.narg('episodes_aired'), episodes_aired),
episodes_all = COALESCE(sqlc.narg('episodes_all'), episodes_all),
episodes_len = COALESCE(sqlc.narg('episodes_len'), episodes_len)
WHERE title_id = sqlc.arg('title_id')
RETURNING *;
-- name: GetReviewByID :one
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE review_id = $1;
-- name: CreateReview :one
INSERT INTO reviews (user_id, title_id, image_ids, review_text, creation_date)
VALUES ($1, $2, $3, $4, $5)
RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date;
-- name: UpdateReview :one
UPDATE reviews
SET
image_ids = COALESCE(sqlc.narg('image_ids'), image_ids),
review_text = COALESCE(sqlc.narg('review_text'), review_text)
WHERE review_id = sqlc.arg('review_id')
RETURNING *;
-- name: DeleteReview :exec
DELETE FROM reviews
WHERE review_id = $1;
-- name: ListReviewsByTitle :many
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE title_id = $1
ORDER BY creation_date DESC
LIMIT $2 OFFSET $3;
-- name: ListReviewsByUser :many
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE user_id = $1
ORDER BY creation_date DESC
LIMIT $2 OFFSET $3;
-- name: GetUserTitle :one
SELECT usertitle_id, user_id, title_id, status, rate, review_id
FROM usertitles
WHERE user_id = $1 AND title_id = $2;
-- name: ListUserTitles :many
SELECT usertitle_id, user_id, title_id, status, rate, review_id
FROM usertitles
WHERE user_id = $1
ORDER BY usertitle_id
LIMIT $2 OFFSET $3;
-- name: CreateUserTitle :one
INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING usertitle_id, user_id, title_id, status, rate, review_id;
-- name: UpdateUserTitle :one
UPDATE usertitles
SET
status = COALESCE(sqlc.narg('status'), status),
rate = COALESCE(sqlc.narg('rate'), rate),
review_id = COALESCE(sqlc.narg('review_id'), review_id)
WHERE user_id = $1 AND title_id = $2
RETURNING *;
-- name: DeleteUserTitle :exec
DELETE FROM usertitles
WHERE user_id = $1 AND ($2::int IS NULL OR title_id = $2);
-- name: ListTags :many
SELECT tag_id, tag_names
FROM tags
ORDER BY tag_id
LIMIT $1 OFFSET $2;

View file

@ -1,97 +0,0 @@
-- TODO:
-- title table triggers
-- maybe jsonb constraints
-- actions (delete)
BEGIN;
CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress');
CREATE TYPE storage_type_t AS ENUM ('local', 's3');
CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned');
CREATE TYPE release_season_t AS ENUM ('winter', 'spring', 'summer', 'fall');
CREATE TABLE providers (
provider_id serial PRIMARY KEY,
provider_name varchar(64) NOT NULL
);
CREATE TABLE tags (
tag_id serial PRIMARY KEY,
tag_names jsonb NOT NULL --mb constraints
);
CREATE TABLE images (
image_id serial PRIMARY KEY,
storage_type storage_type_t NOT NULL,
image_path varchar(256) UNIQUE NOT NULL
);
CREATE TABLE users (
user_id serial PRIMARY KEY,
avatar_id int REFERENCES images (image_id),
passhash text NOT NULL,
mail varchar(64) CHECK (mail ~ '[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+'),
nickname varchar(16) NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]+$'),
disp_name varchar(32),
user_desc varchar(512),
creation_date timestamp NOT NULL
);
CREATE TABLE studios (
studio_id serial PRIMARY KEY,
studio_name varchar(64) UNIQUE,
illust_id int REFERENCES images (image_id),
studio_desc text
);
CREATE TABLE titles (
title_id serial PRIMARY KEY,
title_names jsonb NOT NULL,
studio_id int NOT NULL REFERENCES studios,
poster_id int REFERENCES images (image_id),
signal_ids int[] NOT NULL,
title_status title_status_t NOT NULL,
rating float CHECK (rating > 0 AND rating <= 10), --by trigger
rating_count int CHECK (rating_count >= 0), --by trigger
release_year int CHECK (release_year >= 1900),
release_season release_season_t,
season int CHECK (season >= 0),
episodes_aired int CHECK (episodes_aired >= 0),
episodes_all int CHECK (episodes_all >= 0),
episodes_len jsonb,
CHECK ((episodes_aired IS NULL AND episodes_all IS NULL)
OR (episodes_aired IS NOT NULL AND episodes_all IS NOT NULL
AND episodes_aired <= episodes_all))
);
CREATE TABLE reviews (
review_id serial PRIMARY KEY,
user_id int NOT NULL REFERENCES users,
title_id int NOT NULL REFERENCES titles,
image_ids int[],
review_text text NOT NULL,
creation_date timestamp NOT NULL
);
CREATE TABLE usertitles (
usertitle_id serial PRIMARY KEY,
user_id int NOT NULL REFERENCES users,
title_id int NOT NULL REFERENCES titles,
status usertitle_status_t NOT NULL,
rate int CHECK (rate > 0 AND rate <= 10),
review_id int REFERENCES reviews
);
CREATE TABLE title_tags (
PRIMARY KEY (title_id, tag_id),
title_id int NOT NULL REFERENCES titles,
tag_id int NOT NULL REFERENCES tags
);
CREATE TABLE signals (
signal_id serial PRIMARY KEY,
raw_data jsonb NOT NULL,
provider_id int NOT NULL REFERENCES providers,
dirty bool NOT NULL
);
COMMIT;

View file

@ -1,13 +0,0 @@
version: "2"
sql:
- engine: "postgresql"
queries: "query.sql"
schema: "scheme.sql"
gen:
go:
package: "nyanimedb.backend"
out: "sqlc"
sql_package: "pgx/v5"
sql_driver: "github.com/lib/pq"
emit_db_tags: true
emit_exact_table_names: true

View file

@ -1,14 +0,0 @@
version: "2"
sql:
- engine: "postgresql"
queries: "query.sql"
schema: "scheme.sql"
gen:
go:
package: "nyanimedb_backend"
out: "sqlc"
sql_package: "pgx/v5"
sql_driver: "github.com/lib/pq"
emit_db_tags: true
emit_exact_table_names: true
emit_json_tags: true

View file

@ -1,32 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package nyanimedb_backend
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View file

@ -1,266 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package nyanimedb_backend
import (
"database/sql/driver"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
)
type ReleaseSeasonT string
const (
ReleaseSeasonTWinter ReleaseSeasonT = "winter"
ReleaseSeasonTSpring ReleaseSeasonT = "spring"
ReleaseSeasonTSummer ReleaseSeasonT = "summer"
ReleaseSeasonTFall ReleaseSeasonT = "fall"
)
func (e *ReleaseSeasonT) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ReleaseSeasonT(s)
case string:
*e = ReleaseSeasonT(s)
default:
return fmt.Errorf("unsupported scan type for ReleaseSeasonT: %T", src)
}
return nil
}
type NullReleaseSeasonT struct {
ReleaseSeasonT ReleaseSeasonT `json:"release_season_t"`
Valid bool `json:"valid"` // Valid is true if ReleaseSeasonT is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullReleaseSeasonT) Scan(value interface{}) error {
if value == nil {
ns.ReleaseSeasonT, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ReleaseSeasonT.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullReleaseSeasonT) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ReleaseSeasonT), nil
}
type StorageTypeT string
const (
StorageTypeTLocal StorageTypeT = "local"
StorageTypeTS3 StorageTypeT = "s3"
)
func (e *StorageTypeT) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = StorageTypeT(s)
case string:
*e = StorageTypeT(s)
default:
return fmt.Errorf("unsupported scan type for StorageTypeT: %T", src)
}
return nil
}
type NullStorageTypeT struct {
StorageTypeT StorageTypeT `json:"storage_type_t"`
Valid bool `json:"valid"` // Valid is true if StorageTypeT is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullStorageTypeT) Scan(value interface{}) error {
if value == nil {
ns.StorageTypeT, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.StorageTypeT.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullStorageTypeT) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.StorageTypeT), nil
}
type TitleStatusT string
const (
TitleStatusTFinished TitleStatusT = "finished"
TitleStatusTOngoing TitleStatusT = "ongoing"
TitleStatusTPlanned TitleStatusT = "planned"
)
func (e *TitleStatusT) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TitleStatusT(s)
case string:
*e = TitleStatusT(s)
default:
return fmt.Errorf("unsupported scan type for TitleStatusT: %T", src)
}
return nil
}
type NullTitleStatusT struct {
TitleStatusT TitleStatusT `json:"title_status_t"`
Valid bool `json:"valid"` // Valid is true if TitleStatusT is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTitleStatusT) Scan(value interface{}) error {
if value == nil {
ns.TitleStatusT, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TitleStatusT.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTitleStatusT) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TitleStatusT), nil
}
type UsertitleStatusT string
const (
UsertitleStatusTFinished UsertitleStatusT = "finished"
UsertitleStatusTPlanned UsertitleStatusT = "planned"
UsertitleStatusTDropped UsertitleStatusT = "dropped"
UsertitleStatusTInProgress UsertitleStatusT = "in-progress"
)
func (e *UsertitleStatusT) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = UsertitleStatusT(s)
case string:
*e = UsertitleStatusT(s)
default:
return fmt.Errorf("unsupported scan type for UsertitleStatusT: %T", src)
}
return nil
}
type NullUsertitleStatusT struct {
UsertitleStatusT UsertitleStatusT `json:"usertitle_status_t"`
Valid bool `json:"valid"` // Valid is true if UsertitleStatusT is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullUsertitleStatusT) Scan(value interface{}) error {
if value == nil {
ns.UsertitleStatusT, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.UsertitleStatusT.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullUsertitleStatusT) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.UsertitleStatusT), nil
}
type Images struct {
ImageID int32 `db:"image_id" json:"image_id"`
StorageType StorageTypeT `db:"storage_type" json:"storage_type"`
ImagePath string `db:"image_path" json:"image_path"`
}
type Providers struct {
ProviderID int32 `db:"provider_id" json:"provider_id"`
ProviderName string `db:"provider_name" json:"provider_name"`
}
type Reviews struct {
ReviewID int32 `db:"review_id" json:"review_id"`
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
ImageIds []int32 `db:"image_ids" json:"image_ids"`
ReviewText string `db:"review_text" json:"review_text"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
type Signals struct {
SignalID int32 `db:"signal_id" json:"signal_id"`
RawData []byte `db:"raw_data" json:"raw_data"`
ProviderID int32 `db:"provider_id" json:"provider_id"`
Dirty bool `db:"dirty" json:"dirty"`
}
type Studios struct {
StudioID int32 `db:"studio_id" json:"studio_id"`
StudioName pgtype.Text `db:"studio_name" json:"studio_name"`
IllustID pgtype.Int4 `db:"illust_id" json:"illust_id"`
StudioDesc pgtype.Text `db:"studio_desc" json:"studio_desc"`
}
type Tags struct {
TagID int32 `db:"tag_id" json:"tag_id"`
TagNames []byte `db:"tag_names" json:"tag_names"`
}
type TitleTags struct {
TitleID int32 `db:"title_id" json:"title_id"`
TagID int32 `db:"tag_id" json:"tag_id"`
}
type Titles struct {
TitleID int32 `db:"title_id" json:"title_id"`
TitleNames []byte `db:"title_names" json:"title_names"`
StudioID int32 `db:"studio_id" json:"studio_id"`
PosterID pgtype.Int4 `db:"poster_id" json:"poster_id"`
SignalIds []int32 `db:"signal_ids" json:"signal_ids"`
TitleStatus TitleStatusT `db:"title_status" json:"title_status"`
Rating pgtype.Float8 `db:"rating" json:"rating"`
RatingCount pgtype.Int4 `db:"rating_count" json:"rating_count"`
ReleaseYear pgtype.Int4 `db:"release_year" json:"release_year"`
ReleaseSeason NullReleaseSeasonT `db:"release_season" json:"release_season"`
Season pgtype.Int4 `db:"season" json:"season"`
EpisodesAired pgtype.Int4 `db:"episodes_aired" json:"episodes_aired"`
EpisodesAll pgtype.Int4 `db:"episodes_all" json:"episodes_all"`
EpisodesLen []byte `db:"episodes_len" json:"episodes_len"`
}
type Users struct {
UserID int32 `db:"user_id" json:"user_id"`
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Passhash string `db:"passhash" json:"passhash"`
Mail pgtype.Text `db:"mail" json:"mail"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
type Usertitles struct {
UsertitleID int32 `db:"usertitle_id" json:"usertitle_id"`
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
Status UsertitleStatusT `db:"status" json:"status"`
Rate pgtype.Int4 `db:"rate" json:"rate"`
ReviewID pgtype.Int4 `db:"review_id" json:"review_id"`
}

View file

@ -1,712 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: query.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createImage = `-- name: CreateImage :one
INSERT INTO images (storage_type, image_path)
VALUES ($1, $2)
RETURNING image_id, storage_type, image_path
`
type CreateImageParams struct {
StorageType StorageTypeT `db:"storage_type" json:"storage_type"`
ImagePath string `db:"image_path" json:"image_path"`
}
func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Images, error) {
row := q.db.QueryRow(ctx, createImage, arg.StorageType, arg.ImagePath)
var i Images
err := row.Scan(&i.ImageID, &i.StorageType, &i.ImagePath)
return i, err
}
const createReview = `-- name: CreateReview :one
INSERT INTO reviews (user_id, title_id, image_ids, review_text, creation_date)
VALUES ($1, $2, $3, $4, $5)
RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date
`
type CreateReviewParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
ImageIds []int32 `db:"image_ids" json:"image_ids"`
ReviewText string `db:"review_text" json:"review_text"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
func (q *Queries) CreateReview(ctx context.Context, arg CreateReviewParams) (Reviews, error) {
row := q.db.QueryRow(ctx, createReview,
arg.UserID,
arg.TitleID,
arg.ImageIds,
arg.ReviewText,
arg.CreationDate,
)
var i Reviews
err := row.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
)
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date
`
type CreateUserParams struct {
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Passhash string `db:"passhash" json:"passhash"`
Mail pgtype.Text `db:"mail" json:"mail"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
type CreateUserRow struct {
UserID int32 `db:"user_id" json:"user_id"`
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.db.QueryRow(ctx, createUser,
arg.AvatarID,
arg.Passhash,
arg.Mail,
arg.Nickname,
arg.DispName,
arg.UserDesc,
arg.CreationDate,
)
var i CreateUserRow
err := row.Scan(
&i.UserID,
&i.AvatarID,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
)
return i, err
}
const createUserTitle = `-- name: CreateUserTitle :one
INSERT INTO usertitles (user_id, title_id, status, rate, review_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING usertitle_id, user_id, title_id, status, rate, review_id
`
type CreateUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
Status UsertitleStatusT `db:"status" json:"status"`
Rate pgtype.Int4 `db:"rate" json:"rate"`
ReviewID pgtype.Int4 `db:"review_id" json:"review_id"`
}
func (q *Queries) CreateUserTitle(ctx context.Context, arg CreateUserTitleParams) (Usertitles, error) {
row := q.db.QueryRow(ctx, createUserTitle,
arg.UserID,
arg.TitleID,
arg.Status,
arg.Rate,
arg.ReviewID,
)
var i Usertitles
err := row.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
)
return i, err
}
const deleteReview = `-- name: DeleteReview :exec
DELETE FROM reviews
WHERE review_id = $1
`
func (q *Queries) DeleteReview(ctx context.Context, reviewID int32) error {
_, err := q.db.Exec(ctx, deleteReview, reviewID)
return err
}
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users
WHERE user_id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, userID int32) error {
_, err := q.db.Exec(ctx, deleteUser, userID)
return err
}
const deleteUserTitle = `-- name: DeleteUserTitle :exec
DELETE FROM usertitles
WHERE user_id = $1 AND ($2::int IS NULL OR title_id = $2)
`
type DeleteUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
Column2 int32 `db:"column_2" json:"column_2"`
}
func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams) error {
_, err := q.db.Exec(ctx, deleteUserTitle, arg.UserID, arg.Column2)
return err
}
const getImageByID = `-- name: GetImageByID :one
SELECT image_id, storage_type, image_path
FROM images
WHERE image_id = $1
`
func (q *Queries) GetImageByID(ctx context.Context, imageID int32) (Images, error) {
row := q.db.QueryRow(ctx, getImageByID, imageID)
var i Images
err := row.Scan(&i.ImageID, &i.StorageType, &i.ImagePath)
return i, err
}
const getReviewByID = `-- name: GetReviewByID :one
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE review_id = $1
`
func (q *Queries) GetReviewByID(ctx context.Context, reviewID int32) (Reviews, error) {
row := q.db.QueryRow(ctx, getReviewByID, reviewID)
var i Reviews
err := row.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
)
return i, err
}
const getTitleByID = `-- name: GetTitleByID :one
SELECT title_id, title_names, studio_id, poster_id, signal_ids,
title_status, rating, rating_count, release_year, release_season,
season, episodes_aired, episodes_all, episodes_len
FROM titles
WHERE title_id = $1
`
func (q *Queries) GetTitleByID(ctx context.Context, titleID int32) (Titles, error) {
row := q.db.QueryRow(ctx, getTitleByID, titleID)
var i Titles
err := row.Scan(
&i.TitleID,
&i.TitleNames,
&i.StudioID,
&i.PosterID,
&i.SignalIds,
&i.TitleStatus,
&i.Rating,
&i.RatingCount,
&i.ReleaseYear,
&i.ReleaseSeason,
&i.Season,
&i.EpisodesAired,
&i.EpisodesAll,
&i.EpisodesLen,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
FROM users
WHERE user_id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, userID int32) (Users, error) {
row := q.db.QueryRow(ctx, getUserByID, userID)
var i Users
err := row.Scan(
&i.UserID,
&i.AvatarID,
&i.Passhash,
&i.Mail,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
)
return i, err
}
const getUserTitle = `-- name: GetUserTitle :one
SELECT usertitle_id, user_id, title_id, status, rate, review_id
FROM usertitles
WHERE user_id = $1 AND title_id = $2
`
type GetUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
}
func (q *Queries) GetUserTitle(ctx context.Context, arg GetUserTitleParams) (Usertitles, error) {
row := q.db.QueryRow(ctx, getUserTitle, arg.UserID, arg.TitleID)
var i Usertitles
err := row.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
)
return i, err
}
const listReviewsByTitle = `-- name: ListReviewsByTitle :many
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE title_id = $1
ORDER BY creation_date DESC
LIMIT $2 OFFSET $3
`
type ListReviewsByTitleParams struct {
TitleID int32 `db:"title_id" json:"title_id"`
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListReviewsByTitle(ctx context.Context, arg ListReviewsByTitleParams) ([]Reviews, error) {
rows, err := q.db.Query(ctx, listReviewsByTitle, arg.TitleID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Reviews
for rows.Next() {
var i Reviews
if err := rows.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listReviewsByUser = `-- name: ListReviewsByUser :many
SELECT review_id, user_id, title_id, image_ids, review_text, creation_date
FROM reviews
WHERE user_id = $1
ORDER BY creation_date DESC
LIMIT $2 OFFSET $3
`
type ListReviewsByUserParams struct {
UserID int32 `db:"user_id" json:"user_id"`
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListReviewsByUser(ctx context.Context, arg ListReviewsByUserParams) ([]Reviews, error) {
rows, err := q.db.Query(ctx, listReviewsByUser, arg.UserID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Reviews
for rows.Next() {
var i Reviews
if err := rows.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTags = `-- name: ListTags :many
SELECT tag_id, tag_names
FROM tags
ORDER BY tag_id
LIMIT $1 OFFSET $2
`
type ListTagsParams struct {
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListTags(ctx context.Context, arg ListTagsParams) ([]Tags, error) {
rows, err := q.db.Query(ctx, listTags, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Tags
for rows.Next() {
var i Tags
if err := rows.Scan(&i.TagID, &i.TagNames); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTitles = `-- name: ListTitles :many
SELECT title_id, title_names, studio_id, poster_id, signal_ids,
title_status, rating, rating_count, release_year, release_season,
season, episodes_aired, episodes_all, episodes_len
FROM titles
ORDER BY title_id
LIMIT $1 OFFSET $2
`
type ListTitlesParams struct {
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListTitles(ctx context.Context, arg ListTitlesParams) ([]Titles, error) {
rows, err := q.db.Query(ctx, listTitles, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Titles
for rows.Next() {
var i Titles
if err := rows.Scan(
&i.TitleID,
&i.TitleNames,
&i.StudioID,
&i.PosterID,
&i.SignalIds,
&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
}
const listUserTitles = `-- name: ListUserTitles :many
SELECT usertitle_id, user_id, title_id, status, rate, review_id
FROM usertitles
WHERE user_id = $1
ORDER BY usertitle_id
LIMIT $2 OFFSET $3
`
type ListUserTitlesParams struct {
UserID int32 `db:"user_id" json:"user_id"`
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListUserTitles(ctx context.Context, arg ListUserTitlesParams) ([]Usertitles, error) {
rows, err := q.db.Query(ctx, listUserTitles, arg.UserID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Usertitles
for rows.Next() {
var i Usertitles
if err := rows.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listUsers = `-- name: ListUsers :many
SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date
FROM users
ORDER BY user_id
LIMIT $1 OFFSET $2
`
type ListUsersParams struct {
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]Users, error) {
rows, err := q.db.Query(ctx, listUsers, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Users
for rows.Next() {
var i Users
if err := rows.Scan(
&i.UserID,
&i.AvatarID,
&i.Passhash,
&i.Mail,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateReview = `-- name: UpdateReview :one
UPDATE reviews
SET
image_ids = COALESCE($1, image_ids),
review_text = COALESCE($2, review_text)
WHERE review_id = $3
RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date
`
type UpdateReviewParams struct {
ImageIds []int32 `db:"image_ids" json:"image_ids"`
ReviewText pgtype.Text `db:"review_text" json:"review_text"`
ReviewID int32 `db:"review_id" json:"review_id"`
}
func (q *Queries) UpdateReview(ctx context.Context, arg UpdateReviewParams) (Reviews, error) {
row := q.db.QueryRow(ctx, updateReview, arg.ImageIds, arg.ReviewText, arg.ReviewID)
var i Reviews
err := row.Scan(
&i.ReviewID,
&i.UserID,
&i.TitleID,
&i.ImageIds,
&i.ReviewText,
&i.CreationDate,
)
return i, err
}
const updateTitle = `-- name: UpdateTitle :one
UPDATE titles
SET
title_names = COALESCE($1, title_names),
studio_id = COALESCE($2, studio_id),
poster_id = COALESCE($3, poster_id),
signal_ids = COALESCE($4, signal_ids),
title_status = COALESCE($5, title_status),
release_year = COALESCE($6, release_year),
release_season = COALESCE($7, release_season),
episodes_aired = COALESCE($8, episodes_aired),
episodes_all = COALESCE($9, episodes_all),
episodes_len = COALESCE($10, episodes_len)
WHERE title_id = $11
RETURNING title_id, title_names, studio_id, poster_id, signal_ids, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len
`
type UpdateTitleParams struct {
TitleNames []byte `db:"title_names" json:"title_names"`
StudioID pgtype.Int4 `db:"studio_id" json:"studio_id"`
PosterID pgtype.Int4 `db:"poster_id" json:"poster_id"`
SignalIds []int32 `db:"signal_ids" json:"signal_ids"`
TitleStatus NullTitleStatusT `db:"title_status" json:"title_status"`
ReleaseYear pgtype.Int4 `db:"release_year" json:"release_year"`
ReleaseSeason NullReleaseSeasonT `db:"release_season" json:"release_season"`
EpisodesAired pgtype.Int4 `db:"episodes_aired" json:"episodes_aired"`
EpisodesAll pgtype.Int4 `db:"episodes_all" json:"episodes_all"`
EpisodesLen []byte `db:"episodes_len" json:"episodes_len"`
TitleID int32 `db:"title_id" json:"title_id"`
}
func (q *Queries) UpdateTitle(ctx context.Context, arg UpdateTitleParams) (Titles, error) {
row := q.db.QueryRow(ctx, updateTitle,
arg.TitleNames,
arg.StudioID,
arg.PosterID,
arg.SignalIds,
arg.TitleStatus,
arg.ReleaseYear,
arg.ReleaseSeason,
arg.EpisodesAired,
arg.EpisodesAll,
arg.EpisodesLen,
arg.TitleID,
)
var i Titles
err := row.Scan(
&i.TitleID,
&i.TitleNames,
&i.StudioID,
&i.PosterID,
&i.SignalIds,
&i.TitleStatus,
&i.Rating,
&i.RatingCount,
&i.ReleaseYear,
&i.ReleaseSeason,
&i.Season,
&i.EpisodesAired,
&i.EpisodesAll,
&i.EpisodesLen,
)
return i, err
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
avatar_id = COALESCE($1, avatar_id),
disp_name = COALESCE($2, disp_name),
user_desc = COALESCE($3, user_desc)
WHERE user_id = $4
RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date
`
type UpdateUserParams struct {
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
UserID int32 `db:"user_id" json:"user_id"`
}
type UpdateUserRow struct {
UserID int32 `db:"user_id" json:"user_id"`
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) {
row := q.db.QueryRow(ctx, updateUser,
arg.AvatarID,
arg.DispName,
arg.UserDesc,
arg.UserID,
)
var i UpdateUserRow
err := row.Scan(
&i.UserID,
&i.AvatarID,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
)
return i, err
}
const updateUserTitle = `-- name: UpdateUserTitle :one
UPDATE usertitles
SET
status = COALESCE($3, status),
rate = COALESCE($4, rate),
review_id = COALESCE($5, review_id)
WHERE user_id = $1 AND title_id = $2
RETURNING usertitle_id, user_id, title_id, status, rate, review_id
`
type UpdateUserTitleParams struct {
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
Status NullUsertitleStatusT `db:"status" json:"status"`
Rate pgtype.Int4 `db:"rate" json:"rate"`
ReviewID pgtype.Int4 `db:"review_id" json:"review_id"`
}
func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams) (Usertitles, error) {
row := q.db.QueryRow(ctx, updateUserTitle,
arg.UserID,
arg.TitleID,
arg.Status,
arg.Rate,
arg.ReviewID,
)
var i Usertitles
err := row.Scan(
&i.UsertitleID,
&i.UserID,
&i.TitleID,
&i.Status,
&i.Rate,
&i.ReviewID,
)
return i, err
}

View file

@ -2,7 +2,7 @@
// versions:
// sqlc v1.30.0
package db
package sqlc
import (
"context"

View file

@ -0,0 +1,20 @@
DROP TRIGGER IF EXISTS trg_update_title_rating ON usertitles;
DROP TRIGGER IF EXISTS trg_notify_new_signal ON signals;
DROP FUNCTION IF EXISTS update_title_rating();
DROP FUNCTION IF EXISTS notify_new_signal();
DROP TABLE IF EXISTS signals;
DROP TABLE IF EXISTS title_tags;
DROP TABLE IF EXISTS usertitles;
DROP TABLE IF EXISTS titles;
DROP TABLE IF EXISTS studios;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS images;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS providers;
DROP TYPE IF EXISTS release_season_t;
DROP TYPE IF EXISTS title_status_t;
DROP TYPE IF EXISTS storage_type_t;
DROP TYPE IF EXISTS usertitle_status_t;

View file

@ -0,0 +1,141 @@
-- TODO:
-- maybe jsonb constraints
-- clean unused images
CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress');
CREATE TYPE storage_type_t AS ENUM ('local', 's3');
CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned');
CREATE TYPE release_season_t AS ENUM ('winter', 'spring', 'summer', 'fall');
CREATE TABLE providers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
provider_name text NOT NULL,
credentials jsonb
);
CREATE TABLE tags (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tag_names jsonb NOT NULL
);
CREATE TABLE images (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
storage_type storage_type_t NOT NULL,
image_path text UNIQUE NOT NULL
);
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
avatar_id bigint REFERENCES images (id),
passhash text NOT NULL,
mail text CHECK (mail ~ '[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+'),
nickname text NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]+$'),
disp_name text,
user_desc text,
creation_date timestamptz NOT NULL,
last_login timestamptz
);
CREATE TABLE studios (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
studio_name text UNIQUE,
illust_id bigint REFERENCES images (id),
studio_desc text
);
CREATE TABLE titles (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title_names jsonb NOT NULL,
studio_id bigint NOT NULL REFERENCES studios (id),
poster_id bigint REFERENCES images (id),
title_status title_status_t NOT NULL,
rating float CHECK (rating >= 0 AND rating <= 10),
rating_count int CHECK (rating_count >= 0),
release_year int CHECK (release_year >= 1900),
release_season release_season_t,
season int CHECK (season >= 0),
episodes_aired int CHECK (episodes_aired >= 0),
episodes_all int CHECK (episodes_all >= 0),
episodes_len jsonb,
CHECK ((episodes_aired IS NULL AND episodes_all IS NULL)
OR (episodes_aired IS NOT NULL AND episodes_all IS NOT NULL
AND episodes_aired <= episodes_all))
);
CREATE TABLE usertitles (
PRIMARY KEY (user_id, title_id),
user_id bigint NOT NULL REFERENCES users (id),
title_id bigint NOT NULL REFERENCES titles (id),
status usertitle_status_t NOT NULL,
rate int CHECK (rate > 0 AND rate <= 10),
review_text text,
review_date timestamptz
);
CREATE TABLE title_tags (
PRIMARY KEY (title_id, tag_id),
title_id bigint NOT NULL REFERENCES titles (id),
tag_id bigint NOT NULL REFERENCES tags (id)
);
CREATE TABLE signals (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title_id bigint REFERENCES titles (id),
raw_data jsonb NOT NULL,
provider_id bigint NOT NULL REFERENCES providers (id),
pending boolean NOT NULL
);
-- Functions
CREATE OR REPLACE FUNCTION update_title_rating()
RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE' AND NEW.rate IS DISTINCT FROM OLD.rate) THEN
UPDATE titles
SET
rating = sub.avg_rating,
rating_count = sub.rating_count
FROM (
SELECT
title_id,
AVG(rate)::float AS avg_rating,
COUNT(rate) AS rating_count
FROM usertitles
WHERE title_id = NEW.title_id AND rate IS NOT NULL
GROUP BY title_id
) AS sub
WHERE titles.id = sub.title_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION notify_new_signal()
RETURNS TRIGGER AS $$
DECLARE
payload JSON;
BEGIN
payload := json_build_object(
'signal_id', NEW.id,
'title_id', NEW.title_id,
'provider_id', NEW.provider_id,
'pending', NEW.pending,
'timestamp', NOW()
);
PERFORM pg_notify('new_signal', payload::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Triggers
CREATE TRIGGER trg_update_title_rating
AFTER INSERT OR UPDATE OF rate ON usertitles
FOR EACH ROW
EXECUTE FUNCTION update_title_rating();
CREATE TRIGGER trg_notify_new_signal
AFTER INSERT ON signals
FOR EACH ROW
EXECUTE FUNCTION notify_new_signal();

View file

@ -2,11 +2,12 @@
// versions:
// sqlc v1.30.0
package db
package sqlc
import (
"database/sql/driver"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
@ -184,83 +185,76 @@ func (ns NullUsertitleStatusT) Value() (driver.Value, error) {
return string(ns.UsertitleStatusT), nil
}
type Images struct {
ImageID int32 `db:"image_id" json:"image_id"`
StorageType StorageTypeT `db:"storage_type" json:"storage_type"`
ImagePath string `db:"image_path" json:"image_path"`
type Image struct {
ID int64 `json:"id"`
StorageType StorageTypeT `json:"storage_type"`
ImagePath string `json:"image_path"`
}
type Providers struct {
ProviderID int32 `db:"provider_id" json:"provider_id"`
ProviderName string `db:"provider_name" json:"provider_name"`
type Provider struct {
ID int64 `json:"id"`
ProviderName string `json:"provider_name"`
Credentials []byte `json:"credentials"`
}
type Reviews struct {
ReviewID int32 `db:"review_id" json:"review_id"`
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
ImageIds []int32 `db:"image_ids" json:"image_ids"`
ReviewText string `db:"review_text" json:"review_text"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
type Signal struct {
ID int64 `json:"id"`
TitleID *int64 `json:"title_id"`
RawData []byte `json:"raw_data"`
ProviderID int64 `json:"provider_id"`
Pending bool `json:"pending"`
}
type Signals struct {
SignalID int32 `db:"signal_id" json:"signal_id"`
RawData []byte `db:"raw_data" json:"raw_data"`
ProviderID int32 `db:"provider_id" json:"provider_id"`
Dirty bool `db:"dirty" json:"dirty"`
type Studio struct {
ID int64 `json:"id"`
StudioName *string `json:"studio_name"`
IllustID *int64 `json:"illust_id"`
StudioDesc *string `json:"studio_desc"`
}
type Studios struct {
StudioID int32 `db:"studio_id" json:"studio_id"`
StudioName pgtype.Text `db:"studio_name" json:"studio_name"`
IllustID pgtype.Int4 `db:"illust_id" json:"illust_id"`
StudioDesc pgtype.Text `db:"studio_desc" json:"studio_desc"`
type Tag struct {
ID int64 `json:"id"`
TagNames []byte `json:"tag_names"`
}
type Tags struct {
TagID int32 `db:"tag_id" json:"tag_id"`
TagNames []byte `db:"tag_names" json:"tag_names"`
type Title 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 NullReleaseSeasonT `json:"release_season"`
Season *int32 `json:"season"`
EpisodesAired *int32 `json:"episodes_aired"`
EpisodesAll *int32 `json:"episodes_all"`
EpisodesLen []byte `json:"episodes_len"`
}
type TitleTags struct {
TitleID int32 `db:"title_id" json:"title_id"`
TagID int32 `db:"tag_id" json:"tag_id"`
type TitleTag struct {
TitleID int64 `json:"title_id"`
TagID int64 `json:"tag_id"`
}
type Titles struct {
TitleID int32 `db:"title_id" json:"title_id"`
TitleNames []byte `db:"title_names" json:"title_names"`
StudioID int32 `db:"studio_id" json:"studio_id"`
PosterID pgtype.Int4 `db:"poster_id" json:"poster_id"`
SignalIds []int32 `db:"signal_ids" json:"signal_ids"`
TitleStatus TitleStatusT `db:"title_status" json:"title_status"`
Rating pgtype.Float8 `db:"rating" json:"rating"`
RatingCount pgtype.Int4 `db:"rating_count" json:"rating_count"`
ReleaseYear pgtype.Int4 `db:"release_year" json:"release_year"`
ReleaseSeason NullReleaseSeasonT `db:"release_season" json:"release_season"`
Season pgtype.Int4 `db:"season" json:"season"`
EpisodesAired pgtype.Int4 `db:"episodes_aired" json:"episodes_aired"`
EpisodesAll pgtype.Int4 `db:"episodes_all" json:"episodes_all"`
EpisodesLen []byte `db:"episodes_len" json:"episodes_len"`
type User struct {
ID int64 `json:"id"`
AvatarID *int64 `json:"avatar_id"`
Passhash string `json:"passhash"`
Mail *string `json:"mail"`
Nickname string `json:"nickname"`
DispName *string `json:"disp_name"`
UserDesc *string `json:"user_desc"`
CreationDate time.Time `json:"creation_date"`
LastLogin pgtype.Timestamptz `json:"last_login"`
}
type Users struct {
UserID int32 `db:"user_id" json:"user_id"`
AvatarID pgtype.Int4 `db:"avatar_id" json:"avatar_id"`
Passhash string `db:"passhash" json:"passhash"`
Mail pgtype.Text `db:"mail" json:"mail"`
Nickname string `db:"nickname" json:"nickname"`
DispName pgtype.Text `db:"disp_name" json:"disp_name"`
UserDesc pgtype.Text `db:"user_desc" json:"user_desc"`
CreationDate pgtype.Timestamp `db:"creation_date" json:"creation_date"`
}
type Usertitles struct {
UsertitleID int32 `db:"usertitle_id" json:"usertitle_id"`
UserID int32 `db:"user_id" json:"user_id"`
TitleID int32 `db:"title_id" json:"title_id"`
Status UsertitleStatusT `db:"status" json:"status"`
Rate pgtype.Int4 `db:"rate" json:"rate"`
ReviewID pgtype.Int4 `db:"review_id" json:"review_id"`
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"`
}

73
sql/queries.sql.go Normal file
View file

@ -0,0 +1,73 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: queries.sql
package sqlc
import (
"context"
"time"
)
const createImage = `-- name: CreateImage :one
INSERT INTO images (storage_type, image_path)
VALUES ($1, $2)
RETURNING id, storage_type, image_path
`
type CreateImageParams struct {
StorageType StorageTypeT `json:"storage_type"`
ImagePath string `json:"image_path"`
}
func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Image, error) {
row := q.db.QueryRow(ctx, createImage, arg.StorageType, arg.ImagePath)
var i Image
err := row.Scan(&i.ID, &i.StorageType, &i.ImagePath)
return i, err
}
const getImageByID = `-- name: GetImageByID :one
SELECT id, storage_type, image_path
FROM images
WHERE id = $1
`
func (q *Queries) GetImageByID(ctx context.Context, id int64) (Image, error) {
row := q.db.QueryRow(ctx, getImageByID, id)
var i Image
err := row.Scan(&i.ID, &i.StorageType, &i.ImagePath)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date
FROM users
WHERE id = $1
`
type GetUserByIDRow struct {
ID int64 `json:"id"`
AvatarID *int64 `json:"avatar_id"`
Mail *string `json:"mail"`
Nickname string `json:"nickname"`
DispName *string `json:"disp_name"`
UserDesc *string `json:"user_desc"`
CreationDate time.Time `json:"creation_date"`
}
func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error) {
row := q.db.QueryRow(ctx, getUserByID, id)
var i GetUserByIDRow
err := row.Scan(
&i.ID,
&i.AvatarID,
&i.Mail,
&i.Nickname,
&i.DispName,
&i.UserDesc,
&i.CreationDate,
)
return i, err
}

27
sql/sqlc.yaml Normal file
View file

@ -0,0 +1,27 @@
version: "2"
sql:
- engine: "postgresql"
queries:
- "../modules/backend/queries.sql"
schema: "migrations"
gen:
go:
package: "sqlc"
out: "."
sql_package: "pgx/v5"
sql_driver: "github.com/jackc/pgx/v5"
emit_json_tags: true
emit_pointers_for_null_types: true
overrides:
- db_type: "uuid"
nullable: false
go_type:
import: "github.com/gofrs/uuid"
package: "gofrsuuid"
type: UUID
pointer: true
- db_type: "timestamptz"
nullable: false
go_type:
import: "time"
type: "Time"