Compare commits
17 commits
102d15c5fd
...
6d538ed154
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d538ed154 | |||
| 6ed47b667c | |||
| 66281838b2 | |||
| feb763509e | |||
| fd1e129a5f | |||
| 92c06bbb50 | |||
| e12812d202 | |||
| 948e036e8c | |||
| 71e2661fb9 | |||
| db53ae04e3 | |||
| fd0ca4411b | |||
| 827431bb2f | |||
| 0b5ea285de | |||
| a0df0dff00 | |||
| f2188f6363 | |||
| 8e44b8b7b4 | |||
| 28c38ca1a0 |
52 changed files with 2382 additions and 7250 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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
200
api/api.gen.go
Normal 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(), ¶ms.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
1958
api/gen.go
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
1016
api/openapi.yaml
1016
api/openapi.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -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
3
deploy/generate.sh
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
19
modules/backend/handlers/common.go
Normal file
19
modules/backend/handlers/common.go
Normal 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
|
||||
}
|
||||
50
modules/backend/handlers/users.go
Normal file
50
modules/backend/handlers/users.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
142
modules/backend/queries.sql
Normal 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;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ .title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ .message }}</h1>
|
||||
</body>
|
||||
</html>
|
||||
29
modules/frontend/nginx-default.conf
Normal file
29
modules/frontend/nginx-default.conf
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
238
modules/frontend/package-lock.json
generated
238
modules/frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
25
modules/frontend/src/api/core/ApiError.ts
Normal file
25
modules/frontend/src/api/core/ApiError.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
17
modules/frontend/src/api/core/ApiRequestOptions.ts
Normal file
17
modules/frontend/src/api/core/ApiRequestOptions.ts
Normal 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>;
|
||||
};
|
||||
11
modules/frontend/src/api/core/ApiResult.ts
Normal file
11
modules/frontend/src/api/core/ApiResult.ts
Normal 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;
|
||||
};
|
||||
131
modules/frontend/src/api/core/CancelablePromise.ts
Normal file
131
modules/frontend/src/api/core/CancelablePromise.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
32
modules/frontend/src/api/core/OpenAPI.ts
Normal file
32
modules/frontend/src/api/core/OpenAPI.ts
Normal 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,
|
||||
};
|
||||
323
modules/frontend/src/api/core/request.ts
Normal file
323
modules/frontend/src/api/core/request.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
16
modules/frontend/src/api/index.ts
Normal file
16
modules/frontend/src/api/index.ts
Normal 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';
|
||||
5
modules/frontend/src/api/models/Review.ts
Normal file
5
modules/frontend/src/api/models/Review.ts
Normal 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>;
|
||||
5
modules/frontend/src/api/models/Tag.ts
Normal file
5
modules/frontend/src/api/models/Tag.ts
Normal 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>;
|
||||
5
modules/frontend/src/api/models/Title.ts
Normal file
5
modules/frontend/src/api/models/Title.ts
Normal 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>;
|
||||
35
modules/frontend/src/api/models/User.ts
Normal file
35
modules/frontend/src/api/models/User.ts
Normal 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;
|
||||
};
|
||||
|
||||
5
modules/frontend/src/api/models/UserTitle.ts
Normal file
5
modules/frontend/src/api/models/UserTitle.ts
Normal 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>;
|
||||
35
modules/frontend/src/api/services/DefaultService.ts
Normal file
35
modules/frontend/src/api/services/DefaultService.ts
Normal 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`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
97
modules/frontend/src/components/UserPage/UserPage.module.css
Normal file
97
modules/frontend/src/components/UserPage/UserPage.module.css
Normal 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;
|
||||
}
|
||||
64
modules/frontend/src/components/UserPage/UserPage.tsx
Normal file
64
modules/frontend/src/components/UserPage/UserPage.tsx
Normal 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;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module tutorial.sqlc.dev/app
|
||||
|
||||
go 1.25.2
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package db
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
20
sql/migrations/000001_init.down.sql
Normal file
20
sql/migrations/000001_init.down.sql
Normal 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;
|
||||
141
sql/migrations/000001_init.up.sql
Normal file
141
sql/migrations/000001_init.up.sql
Normal 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();
|
||||
|
|
@ -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
73
sql/queries.sql.go
Normal 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
27
sql/sqlc.yaml
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue