diff --git a/.forgejo/workflows/build-and-deploy.yml b/.forgejo/workflows/build-and-deploy.yml index 3c473d2..e7d0a83 100644 --- a/.forgejo/workflows/build-and-deploy.yml +++ b/.forgejo/workflows/build-and-deploy.yml @@ -18,10 +18,14 @@ jobs: - uses: actions/setup-go@v6 with: go-version: '^1.25' + check-latest: false + cache-dependency-path: | + modules/backend/go.sum - - name: Build backend + - name: Build Go app run: | cd modules/backend + go mod tidy go build -o nyanimedb . tar -czvf nyanimedb-backend.tar.gz nyanimedb @@ -31,18 +35,6 @@ jobs: name: nyanimedb-backend.tar.gz path: modules/backend/nyanimedb-backend.tar.gz - - name: Build auth - run: | - cd modules/auth - go build -o auth . - tar -czvf nyanimedb-auth.tar.gz auth - - - name: Upload built auth to artifactory - uses: actions/upload-artifact@v3 - with: - name: nyanimedb-auth.tar.gz - path: modules/auth/nyanimedb-auth.tar.gz - # Build frontend - uses: actions/setup-node@v5 with: @@ -84,14 +76,6 @@ jobs: push: true tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest - - name: Build and push auth image - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfiles/Dockerfile_auth - push: true - tags: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest - - name: Build and push frontend image uses: docker/build-push-action@v6 with: @@ -101,7 +85,7 @@ jobs: tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest deploy: - runs-on: debian-test + runs-on: self-hosted needs: build env: POSTGRES_USER: ${{ secrets.POSTGRES_USER }} diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index e85ddf9..5ff77e0 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -11,57 +11,47 @@ paths: parameters: - $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/title_sort' - - name: sort_forward - in: query + - in: query + name: sort_forward schema: type: boolean default: true - - name: ext_search - in: query - schema: - type: boolean - default: false - - name: word - in: query + - in: query + name: word schema: type: string - - name: status - in: query - description: List of title statuses to filter + - in: query + name: status schema: - type: array - items: - $ref: '#/components/schemas/TitleStatus' - explode: false - style: form - - name: rating - in: query + $ref: '#/components/schemas/TitleStatus' + - in: query + name: rating schema: type: number format: double - - name: release_year - in: query + - in: query + name: release_year schema: type: integer format: int32 - - name: release_season - in: query + - in: query + name: release_season schema: $ref: '#/components/schemas/ReleaseSeason' - - name: limit - in: query + - in: query + name: limit schema: type: integer format: int32 default: 10 - - name: offset - in: query + - in: query + name: offset schema: type: integer format: int32 default: 0 - - name: fields - in: query + - in: query + name: fields schema: type: string default: all @@ -74,10 +64,10 @@ paths: type: object properties: data: - description: List of titles type: array items: $ref: '#/components/schemas/Title' + description: List of titles cursor: $ref: '#/components/schemas/CursorObj' required: @@ -91,17 +81,16 @@ paths: description: Unknown server error '/titles/{title_id}': get: - operationId: getTitle summary: Get title description parameters: - - name: title_id - in: path + - in: path + name: title_id required: true schema: type: integer format: int64 - - name: fields - in: query + - in: query + name: fields schema: type: string default: all @@ -122,16 +111,15 @@ paths: description: Unknown server error '/users/{user_id}': get: - operationId: getUsersId summary: Get user info parameters: - - name: user_id - in: path + - in: path + name: user_id required: true schema: type: string - - name: fields - in: query + - in: query + name: fields schema: type: string default: all @@ -148,146 +136,50 @@ paths: description: User not found '500': description: Unknown server error - patch: - operationId: updateUser - summary: Partially update a user account - description: | - Update selected user profile fields (excluding password). - Password updates must be done via the dedicated auth-service (`/auth/`). - Fields not provided in the request body remain unchanged. - parameters: - - name: user_id - in: path - description: User ID (primary key) - required: true - schema: - type: integer - format: int64 - example: 123 - requestBody: - required: true - content: - application/json: - schema: - description: Only provided fields are updated. Omitted fields remain unchanged. - type: object - properties: - avatar_id: - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - type: integer - format: int64 - example: 42 - nullable: true - mail: - description: User email (must be unique and valid) - type: string - format: email - example: john.doe.updated@example.com - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - nickname: - description: 'Username (alphanumeric + `_` or `-`, 3–16 chars)' - type: string - example: john_doe_43 - maxLength: 16 - minLength: 3 - pattern: '^[a-zA-Z0-9_-]{3,16}$' - disp_name: - description: Display name - type: string - example: John Smith - maxLength: 32 - user_desc: - description: User description / bio - type: string - example: Just a curious developer. - maxLength: 512 - additionalProperties: false - responses: - '200': - description: User updated successfully. Returns updated user representation (excluding sensitive fields). - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '400': - description: 'Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)' - '401': - description: Unauthorized — missing or invalid authentication token - '403': - description: 'Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)' - '404': - description: User not found - '409': - description: 'Conflict — e.g., requested `nickname` or `mail` already taken by another user' - '422': - description: 'Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)' - '500': - description: Unknown server error - '/users/{user_id}/titles': + '/users/{user_id}/titles/': get: - operationId: getUserTitles summary: Get user titles parameters: - $ref: '#/components/parameters/cursor' - - $ref: '#/components/parameters/title_sort' - - name: user_id - in: path + - in: path + name: user_id required: true schema: type: string - - name: sort_forward - in: query - schema: - type: boolean - default: true - - name: word - in: query + - in: query + name: word schema: type: string - - name: status - in: query - description: List of title statuses to filter + - in: query + name: status schema: - type: array - items: - $ref: '#/components/schemas/TitleStatus' - explode: false - style: form - - name: watch_status - in: query + $ref: '#/components/schemas/TitleStatus' + - in: query + name: watch_status schema: - type: array - items: - $ref: '#/components/schemas/UserTitleStatus' - explode: false - style: form - - name: rating - in: query + $ref: '#/components/schemas/UserTitleStatus' + - in: query + name: rating schema: type: number format: double - - name: my_rate - in: query + - in: query + name: release_year schema: type: integer format: int32 - - name: release_year - in: query - schema: - type: integer - format: int32 - - name: release_season - in: query + - in: query + name: release_season schema: $ref: '#/components/schemas/ReleaseSeason' - - name: limit - in: query + - in: query + name: limit schema: type: integer format: int32 default: 10 - - name: fields - in: query + - in: query + name: fields schema: type: string default: all @@ -297,181 +189,15 @@ paths: content: application/json: schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/UserTitle' - cursor: - $ref: '#/components/schemas/CursorObj' - required: - - data - - cursor + type: array + items: + $ref: '#/components/schemas/UserTitle' '204': description: No titles found '400': description: Request params are not correct - '404': - description: User not found '500': description: Unknown server error - post: - operationId: addUserTitle - summary: Add a title to a user - description: 'User adding title to list af watched, status required' - parameters: - - name: user_id - in: path - description: ID of the user to assign the title to - required: true - schema: - type: integer - format: int64 - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - title_id: - type: integer - format: int64 - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - required: - - title_id - - status - responses: - '200': - description: Title successfully added to user - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitleMini' - '400': - description: 'Invalid request body (missing fields, invalid types, etc.)' - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to assign titles to this user - '404': - description: User or Title not found - '409': - description: Conflict — title already assigned to user (if applicable) - '500': - description: Internal server error - '/users/{user_id}/titles/{title_id}': - get: - operationId: getUserTitle - summary: Get user title - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: User titles - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitleMini' - '204': - description: No user title found - '400': - description: Request params are not correct - '404': - description: User or title not found - '500': - description: Unknown server error - patch: - operationId: updateUserTitle - summary: Update a usertitle - description: User updating title list of watched - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - responses: - '200': - description: Title successfully updated - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitleMini' - '400': - description: 'Invalid request body (missing fields, invalid types, etc.)' - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to update title - '404': - description: User or Title not found - '500': - description: Internal server error - delete: - operationId: deleteUserTitle - summary: Delete a usertitle - description: User deleting title from list of watched - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: Title successfully deleted - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to delete title - '404': - description: User or Title not found - '500': - description: Internal server error components: parameters: cursor: @@ -487,36 +213,25 @@ components: schema: $ref: '#/components/schemas/TitleSort' schemas: + CursorObj: + type: object + required: + - id + properties: + id: + type: integer + format: int64 + param: + type: string TitleSort: - description: Title sort order type: string + description: Title sort order default: id enum: - id - year - rating - views - TitleStatus: - description: Title status - type: string - enum: - - finished - - ongoing - - planned - ReleaseSeason: - description: Title release season - type: string - enum: - - winter - - spring - - summer - - fall - StorageType: - description: Image storage type - type: string - enum: - - s3 - - local Image: type: object properties: @@ -524,11 +239,61 @@ components: type: integer format: int64 storage_type: - $ref: '#/components/schemas/StorageType' + type: string image_path: type: string + TitleStatus: + type: string + description: Title status + enum: + - finished + - ongoing + - planned + ReleaseSeason: + type: string + description: Title release season + enum: + - winter + - spring + - summer + - fall + UserTitleStatus: + type: string + description: User's title status + enum: + - finished + - planned + - dropped + - in-progress + Review: + type: object + additionalProperties: true + Tag: + type: object + description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' + additionalProperties: + type: string + example: + en: Shojo + ru: Сёдзё + ja: 少女 + Tags: + type: array + description: Array of localized tags + items: + $ref: '#/components/schemas/Tag' + example: + - en: Shojo + ru: Сёдзё + ja: 少女 + - en: Shounen + ru: Сёнен + ja: 少年 Studio: type: object + required: + - id + - name properties: id: type: integer @@ -539,50 +304,21 @@ components: $ref: '#/components/schemas/Image' description: type: string - required: - - id - - name - Tag: - description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' - type: object - example: - en: Shojo - ru: Сёдзё - ja: 少女 - additionalProperties: - type: string - Tags: - description: Array of localized tags - type: array - items: - $ref: '#/components/schemas/Tag' - example: - - en: Shojo - ru: Сёдзё - ja: 少女 - - en: Shounen - ru: Сёнен - ja: 少年 Title: type: object + required: + - id + - title_names + - tags properties: id: - description: Unique title ID (primary key) type: integer format: int64 + description: Unique title ID (primary key) example: 1 title_names: - description: 'Localized titles. Key = language (ISO 639-1), value = list of names' type: object - example: - en: - - Attack on Titan - - AoT - ru: - - Атака титанов - - Титаны - ja: - - 進撃の巨人 + description: 'Localized titles. Key = language (ISO 639-1), value = list of names' additionalProperties: type: array items: @@ -592,6 +328,15 @@ components: example: - Attack on Titan - AoT + example: + en: + - Attack on Titan + - AoT + ru: + - Атака титанов + - Титаны + ja: + - 進撃の巨人 studio: $ref: '#/components/schemas/Studio' tags: @@ -622,91 +367,54 @@ components: additionalProperties: type: number format: double - required: - - id - - title_names - - tags - CursorObj: - type: object - properties: - id: - type: integer - format: int64 - param: - type: string - required: - - id + additionalProperties: true User: type: object properties: id: - description: Unique user ID (primary key) type: integer format: int64 + description: Unique user ID (primary key) example: 1 - image: - $ref: '#/components/schemas/Image' + avatar_id: + type: integer + format: int64 + description: ID of the user avatar (references images table) + example: null mail: - description: User email type: string format: email + description: User email example: john.doe@example.com nickname: + type: string description: Username (alphanumeric + _ or -) - type: string - example: john_doe_42 maxLength: 16 + example: john_doe_42 disp_name: + type: string description: Display name - type: string - example: John Doe maxLength: 32 + example: John Doe user_desc: - description: User description type: string - example: Just a regular user. + description: User description maxLength: 512 + example: Just a regular user. creation_date: - description: Timestamp when the user was created type: string format: date-time + description: Timestamp when the user was created example: '2025-10-10T23:45:47.908073Z' required: - user_id - nickname - UserTitleStatus: - description: User's title status - type: string - enum: - - finished - - planned - - dropped - - in-progress UserTitle: type: object - properties: - user_id: - type: integer - format: int64 - title: - $ref: '#/components/schemas/Title' - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time required: - user_id - title_id - status - UserTitleMini: - type: object properties: user_id: type: integer @@ -725,10 +433,4 @@ components: ctime: type: string format: date-time - required: - - user_id - - title_id - - status - Review: - type: object additionalProperties: true diff --git a/api/api.gen.go b/api/api.gen.go index c8fd9aa..f252a5a 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -24,12 +24,6 @@ const ( Winter ReleaseSeason = "winter" ) -// Defines values for StorageType. -const ( - Local StorageType = "local" - S3 StorageType = "s3" -) - // Defines values for TitleSort. const ( Id TitleSort = "id" @@ -61,19 +55,14 @@ type CursorObj struct { // Image defines model for Image. type Image struct { - Id *int64 `json:"id,omitempty"` - ImagePath *string `json:"image_path,omitempty"` - - // StorageType Image storage type - StorageType *StorageType `json:"storage_type,omitempty"` + Id *int64 `json:"id,omitempty"` + ImagePath *string `json:"image_path,omitempty"` + StorageType *string `json:"storage_type,omitempty"` } // ReleaseSeason Title release season type ReleaseSeason string -// StorageType Image storage type -type StorageType string - // Studio defines model for Studio. type Studio struct { Description *string `json:"description,omitempty"` @@ -112,7 +101,8 @@ type Title struct { TitleNames map[string][]string `json:"title_names"` // TitleStatus Title status - TitleStatus *TitleStatus `json:"title_status,omitempty"` + TitleStatus *TitleStatus `json:"title_status,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` } // TitleSort Title sort order @@ -123,6 +113,9 @@ type TitleStatus string // User defines model for User. type User struct { + // AvatarId ID of the user avatar (references images table) + AvatarId *int64 `json:"avatar_id,omitempty"` + // CreationDate Timestamp when the user was created CreationDate *time.Time `json:"creation_date,omitempty"` @@ -130,8 +123,7 @@ type User struct { DispName *string `json:"disp_name,omitempty"` // Id Unique user ID (primary key) - Id *int64 `json:"id,omitempty"` - Image *Image `json:"image,omitempty"` + Id *int64 `json:"id,omitempty"` // Mail User email Mail *openapi_types.Email `json:"mail,omitempty"` @@ -150,21 +142,10 @@ type UserTitle struct { ReviewId *int64 `json:"review_id,omitempty"` // Status User's title status - Status UserTitleStatus `json:"status"` - Title *Title `json:"title,omitempty"` - UserId int64 `json:"user_id"` -} - -// UserTitleMini defines model for UserTitleMini. -type UserTitleMini struct { - Ctime *time.Time `json:"ctime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - ReviewId *int64 `json:"review_id,omitempty"` - - // Status User's title status - Status UserTitleStatus `json:"status"` - TitleId int64 `json:"title_id"` - UserId int64 `json:"user_id"` + Status UserTitleStatus `json:"status"` + TitleId int64 `json:"title_id"` + UserId int64 `json:"user_id"` + AdditionalProperties map[string]interface{} `json:"-"` } // UserTitleStatus User's title status @@ -175,14 +156,11 @@ type Cursor = string // GetTitlesParams defines parameters for GetTitles. type GetTitlesParams struct { - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` - SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` - ExtSearch *bool `form:"ext_search,omitempty" json:"ext_search,omitempty"` - Word *string `form:"word,omitempty" json:"word,omitempty"` - - // Status List of title statuses to filter - Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"` + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` + Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` + SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` + Word *string `form:"word,omitempty" json:"word,omitempty"` + Status *TitleStatus `form:"status,omitempty" json:"status,omitempty"` Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` @@ -191,77 +169,407 @@ type GetTitlesParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } -// GetTitleParams defines parameters for GetTitle. -type GetTitleParams struct { +// GetTitlesTitleIdParams defines parameters for GetTitlesTitleId. +type GetTitlesTitleIdParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } -// GetUsersIdParams defines parameters for GetUsersId. -type GetUsersIdParams struct { +// GetUsersUserIdParams defines parameters for GetUsersUserId. +type GetUsersUserIdParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } -// UpdateUserJSONBody defines parameters for UpdateUser. -type UpdateUserJSONBody struct { - // AvatarId ID of the user avatar (references `images.id`); set to `null` to remove avatar - AvatarId *int64 `json:"avatar_id"` - - // DispName Display name - DispName *string `json:"disp_name,omitempty"` - - // Mail User email (must be unique and valid) - Mail *openapi_types.Email `json:"mail,omitempty"` - - // Nickname Username (alphanumeric + `_` or `-`, 3–16 chars) - Nickname *string `json:"nickname,omitempty"` - - // UserDesc User description / bio - UserDesc *string `json:"user_desc,omitempty"` +// GetUsersUserIdTitlesParams defines parameters for GetUsersUserIdTitles. +type GetUsersUserIdTitlesParams struct { + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` + Word *string `form:"word,omitempty" json:"word,omitempty"` + Status *TitleStatus `form:"status,omitempty" json:"status,omitempty"` + WatchStatus *UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"` + Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` + ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` + ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` + Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` + Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } -// GetUserTitlesParams defines parameters for GetUserTitles. -type GetUserTitlesParams struct { - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` - SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` - Word *string `form:"word,omitempty" json:"word,omitempty"` - - // Status List of title statuses to filter - Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"` - WatchStatus *[]UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"` - Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` - MyRate *int32 `form:"my_rate,omitempty" json:"my_rate,omitempty"` - ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` - ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` - Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` - Fields *string `form:"fields,omitempty" json:"fields,omitempty"` +// Getter for additional properties for Title. Returns the specified +// element and whether it was found +func (a Title) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return } -// AddUserTitleJSONBody defines parameters for AddUserTitle. -type AddUserTitleJSONBody struct { - Rate *int32 `json:"rate,omitempty"` - - // Status User's title status - Status UserTitleStatus `json:"status"` - TitleId int64 `json:"title_id"` +// Setter for additional properties for Title +func (a *Title) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value } -// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle. -type UpdateUserTitleJSONBody struct { - Rate *int32 `json:"rate,omitempty"` +// Override default JSON handling for Title to handle AdditionalProperties +func (a *Title) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } - // Status User's title status - Status *UserTitleStatus `json:"status,omitempty"` + if raw, found := object["episodes_aired"]; found { + err = json.Unmarshal(raw, &a.EpisodesAired) + if err != nil { + return fmt.Errorf("error reading 'episodes_aired': %w", err) + } + delete(object, "episodes_aired") + } + + if raw, found := object["episodes_all"]; found { + err = json.Unmarshal(raw, &a.EpisodesAll) + if err != nil { + return fmt.Errorf("error reading 'episodes_all': %w", err) + } + delete(object, "episodes_all") + } + + if raw, found := object["episodes_len"]; found { + err = json.Unmarshal(raw, &a.EpisodesLen) + if err != nil { + return fmt.Errorf("error reading 'episodes_len': %w", err) + } + delete(object, "episodes_len") + } + + if raw, found := object["id"]; found { + err = json.Unmarshal(raw, &a.Id) + if err != nil { + return fmt.Errorf("error reading 'id': %w", err) + } + delete(object, "id") + } + + if raw, found := object["poster"]; found { + err = json.Unmarshal(raw, &a.Poster) + if err != nil { + return fmt.Errorf("error reading 'poster': %w", err) + } + delete(object, "poster") + } + + if raw, found := object["rating"]; found { + err = json.Unmarshal(raw, &a.Rating) + if err != nil { + return fmt.Errorf("error reading 'rating': %w", err) + } + delete(object, "rating") + } + + if raw, found := object["rating_count"]; found { + err = json.Unmarshal(raw, &a.RatingCount) + if err != nil { + return fmt.Errorf("error reading 'rating_count': %w", err) + } + delete(object, "rating_count") + } + + if raw, found := object["release_season"]; found { + err = json.Unmarshal(raw, &a.ReleaseSeason) + if err != nil { + return fmt.Errorf("error reading 'release_season': %w", err) + } + delete(object, "release_season") + } + + if raw, found := object["release_year"]; found { + err = json.Unmarshal(raw, &a.ReleaseYear) + if err != nil { + return fmt.Errorf("error reading 'release_year': %w", err) + } + delete(object, "release_year") + } + + if raw, found := object["studio"]; found { + err = json.Unmarshal(raw, &a.Studio) + if err != nil { + return fmt.Errorf("error reading 'studio': %w", err) + } + delete(object, "studio") + } + + if raw, found := object["tags"]; found { + err = json.Unmarshal(raw, &a.Tags) + if err != nil { + return fmt.Errorf("error reading 'tags': %w", err) + } + delete(object, "tags") + } + + if raw, found := object["title_names"]; found { + err = json.Unmarshal(raw, &a.TitleNames) + if err != nil { + return fmt.Errorf("error reading 'title_names': %w", err) + } + delete(object, "title_names") + } + + if raw, found := object["title_status"]; found { + err = json.Unmarshal(raw, &a.TitleStatus) + if err != nil { + return fmt.Errorf("error reading 'title_status': %w", err) + } + delete(object, "title_status") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil } -// UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType. -type UpdateUserJSONRequestBody UpdateUserJSONBody +// Override default JSON handling for Title to handle AdditionalProperties +func (a Title) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) -// AddUserTitleJSONRequestBody defines body for AddUserTitle for application/json ContentType. -type AddUserTitleJSONRequestBody AddUserTitleJSONBody + if a.EpisodesAired != nil { + object["episodes_aired"], err = json.Marshal(a.EpisodesAired) + if err != nil { + return nil, fmt.Errorf("error marshaling 'episodes_aired': %w", err) + } + } -// UpdateUserTitleJSONRequestBody defines body for UpdateUserTitle for application/json ContentType. -type UpdateUserTitleJSONRequestBody UpdateUserTitleJSONBody + if a.EpisodesAll != nil { + object["episodes_all"], err = json.Marshal(a.EpisodesAll) + if err != nil { + return nil, fmt.Errorf("error marshaling 'episodes_all': %w", err) + } + } + + if a.EpisodesLen != nil { + object["episodes_len"], err = json.Marshal(a.EpisodesLen) + if err != nil { + return nil, fmt.Errorf("error marshaling 'episodes_len': %w", err) + } + } + + object["id"], err = json.Marshal(a.Id) + if err != nil { + return nil, fmt.Errorf("error marshaling 'id': %w", err) + } + + if a.Poster != nil { + object["poster"], err = json.Marshal(a.Poster) + if err != nil { + return nil, fmt.Errorf("error marshaling 'poster': %w", err) + } + } + + if a.Rating != nil { + object["rating"], err = json.Marshal(a.Rating) + if err != nil { + return nil, fmt.Errorf("error marshaling 'rating': %w", err) + } + } + + if a.RatingCount != nil { + object["rating_count"], err = json.Marshal(a.RatingCount) + if err != nil { + return nil, fmt.Errorf("error marshaling 'rating_count': %w", err) + } + } + + if a.ReleaseSeason != nil { + object["release_season"], err = json.Marshal(a.ReleaseSeason) + if err != nil { + return nil, fmt.Errorf("error marshaling 'release_season': %w", err) + } + } + + if a.ReleaseYear != nil { + object["release_year"], err = json.Marshal(a.ReleaseYear) + if err != nil { + return nil, fmt.Errorf("error marshaling 'release_year': %w", err) + } + } + + if a.Studio != nil { + object["studio"], err = json.Marshal(a.Studio) + if err != nil { + return nil, fmt.Errorf("error marshaling 'studio': %w", err) + } + } + + object["tags"], err = json.Marshal(a.Tags) + if err != nil { + return nil, fmt.Errorf("error marshaling 'tags': %w", err) + } + + object["title_names"], err = json.Marshal(a.TitleNames) + if err != nil { + return nil, fmt.Errorf("error marshaling 'title_names': %w", err) + } + + if a.TitleStatus != nil { + object["title_status"], err = json.Marshal(a.TitleStatus) + if err != nil { + return nil, fmt.Errorf("error marshaling 'title_status': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for UserTitle. Returns the specified +// element and whether it was found +func (a UserTitle) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for UserTitle +func (a *UserTitle) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for UserTitle to handle AdditionalProperties +func (a *UserTitle) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["ctime"]; found { + err = json.Unmarshal(raw, &a.Ctime) + if err != nil { + return fmt.Errorf("error reading 'ctime': %w", err) + } + delete(object, "ctime") + } + + if raw, found := object["rate"]; found { + err = json.Unmarshal(raw, &a.Rate) + if err != nil { + return fmt.Errorf("error reading 'rate': %w", err) + } + delete(object, "rate") + } + + if raw, found := object["review_id"]; found { + err = json.Unmarshal(raw, &a.ReviewId) + if err != nil { + return fmt.Errorf("error reading 'review_id': %w", err) + } + delete(object, "review_id") + } + + if raw, found := object["status"]; found { + err = json.Unmarshal(raw, &a.Status) + if err != nil { + return fmt.Errorf("error reading 'status': %w", err) + } + delete(object, "status") + } + + if raw, found := object["title_id"]; found { + err = json.Unmarshal(raw, &a.TitleId) + if err != nil { + return fmt.Errorf("error reading 'title_id': %w", err) + } + delete(object, "title_id") + } + + if raw, found := object["user_id"]; found { + err = json.Unmarshal(raw, &a.UserId) + if err != nil { + return fmt.Errorf("error reading 'user_id': %w", err) + } + delete(object, "user_id") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for UserTitle to handle AdditionalProperties +func (a UserTitle) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Ctime != nil { + object["ctime"], err = json.Marshal(a.Ctime) + if err != nil { + return nil, fmt.Errorf("error marshaling 'ctime': %w", err) + } + } + + if a.Rate != nil { + object["rate"], err = json.Marshal(a.Rate) + if err != nil { + return nil, fmt.Errorf("error marshaling 'rate': %w", err) + } + } + + if a.ReviewId != nil { + object["review_id"], err = json.Marshal(a.ReviewId) + if err != nil { + return nil, fmt.Errorf("error marshaling 'review_id': %w", err) + } + } + + object["status"], err = json.Marshal(a.Status) + if err != nil { + return nil, fmt.Errorf("error marshaling 'status': %w", err) + } + + object["title_id"], err = json.Marshal(a.TitleId) + if err != nil { + return nil, fmt.Errorf("error marshaling 'title_id': %w", err) + } + + object["user_id"], err = json.Marshal(a.UserId) + if err != nil { + return nil, fmt.Errorf("error marshaling 'user_id': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} // ServerInterface represents all server handlers. type ServerInterface interface { @@ -270,28 +578,13 @@ type ServerInterface interface { GetTitles(c *gin.Context, params GetTitlesParams) // Get title description // (GET /titles/{title_id}) - GetTitle(c *gin.Context, titleId int64, params GetTitleParams) + GetTitlesTitleId(c *gin.Context, titleId int64, params GetTitlesTitleIdParams) // Get user info // (GET /users/{user_id}) - GetUsersId(c *gin.Context, userId string, params GetUsersIdParams) - // Partially update a user account - // (PATCH /users/{user_id}) - UpdateUser(c *gin.Context, userId int64) + GetUsersUserId(c *gin.Context, userId string, params GetUsersUserIdParams) // Get user titles - // (GET /users/{user_id}/titles) - GetUserTitles(c *gin.Context, userId string, params GetUserTitlesParams) - // Add a title to a user - // (POST /users/{user_id}/titles) - AddUserTitle(c *gin.Context, userId int64) - // Delete a usertitle - // (DELETE /users/{user_id}/titles/{title_id}) - DeleteUserTitle(c *gin.Context, userId int64, titleId int64) - // Get user title - // (GET /users/{user_id}/titles/{title_id}) - GetUserTitle(c *gin.Context, userId int64, titleId int64) - // Update a usertitle - // (PATCH /users/{user_id}/titles/{title_id}) - UpdateUserTitle(c *gin.Context, userId int64, titleId int64) + // (GET /users/{user_id}/titles/) + GetUsersUserIdTitles(c *gin.Context, userId string, params GetUsersUserIdTitlesParams) } // ServerInterfaceWrapper converts contexts to parameters. @@ -335,14 +628,6 @@ func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) { return } - // ------------- Optional query parameter "ext_search" ------------- - - err = runtime.BindQueryParameter("form", true, false, "ext_search", c.Request.URL.Query(), ¶ms.ExtSearch) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter ext_search: %w", err), http.StatusBadRequest) - return - } - // ------------- Optional query parameter "word" ------------- err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), ¶ms.Word) @@ -353,7 +638,7 @@ func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) { // ------------- Optional query parameter "status" ------------- - err = runtime.BindQueryParameter("form", false, false, "status", c.Request.URL.Query(), ¶ms.Status) + err = runtime.BindQueryParameter("form", true, false, "status", c.Request.URL.Query(), ¶ms.Status) if err != nil { siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest) return @@ -417,8 +702,8 @@ func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) { siw.Handler.GetTitles(c, params) } -// GetTitle operation middleware -func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { +// GetTitlesTitleId operation middleware +func (siw *ServerInterfaceWrapper) GetTitlesTitleId(c *gin.Context) { var err error @@ -432,7 +717,7 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { } // Parameter object where we will unmarshal all parameters from the context - var params GetTitleParams + var params GetTitlesTitleIdParams // ------------- Optional query parameter "fields" ------------- @@ -449,11 +734,11 @@ func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { } } - siw.Handler.GetTitle(c, titleId, params) + siw.Handler.GetTitlesTitleId(c, titleId, params) } -// GetUsersId operation middleware -func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) { +// GetUsersUserId operation middleware +func (siw *ServerInterfaceWrapper) GetUsersUserId(c *gin.Context) { var err error @@ -467,7 +752,7 @@ func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) { } // Parameter object where we will unmarshal all parameters from the context - var params GetUsersIdParams + var params GetUsersUserIdParams // ------------- Optional query parameter "fields" ------------- @@ -484,35 +769,11 @@ func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) { } } - siw.Handler.GetUsersId(c, userId, params) + siw.Handler.GetUsersUserId(c, userId, params) } -// UpdateUser operation middleware -func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - 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 - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.UpdateUser(c, userId) -} - -// GetUserTitles operation middleware -func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { +// GetUsersUserIdTitles operation middleware +func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { var err error @@ -526,7 +787,7 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { } // Parameter object where we will unmarshal all parameters from the context - var params GetUserTitlesParams + var params GetUsersUserIdTitlesParams // ------------- Optional query parameter "cursor" ------------- @@ -536,22 +797,6 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { return } - // ------------- Optional query parameter "sort" ------------- - - err = runtime.BindQueryParameter("form", true, false, "sort", c.Request.URL.Query(), ¶ms.Sort) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "sort_forward" ------------- - - err = runtime.BindQueryParameter("form", true, false, "sort_forward", c.Request.URL.Query(), ¶ms.SortForward) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort_forward: %w", err), http.StatusBadRequest) - return - } - // ------------- Optional query parameter "word" ------------- err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), ¶ms.Word) @@ -562,7 +807,7 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { // ------------- Optional query parameter "status" ------------- - err = runtime.BindQueryParameter("form", false, false, "status", c.Request.URL.Query(), ¶ms.Status) + err = runtime.BindQueryParameter("form", true, false, "status", c.Request.URL.Query(), ¶ms.Status) if err != nil { siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest) return @@ -570,7 +815,7 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { // ------------- Optional query parameter "watch_status" ------------- - err = runtime.BindQueryParameter("form", false, false, "watch_status", c.Request.URL.Query(), ¶ms.WatchStatus) + err = runtime.BindQueryParameter("form", true, false, "watch_status", c.Request.URL.Query(), ¶ms.WatchStatus) if err != nil { siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter watch_status: %w", err), http.StatusBadRequest) return @@ -584,14 +829,6 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { return } - // ------------- Optional query parameter "my_rate" ------------- - - err = runtime.BindQueryParameter("form", true, false, "my_rate", c.Request.URL.Query(), ¶ms.MyRate) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter my_rate: %w", err), http.StatusBadRequest) - return - } - // ------------- Optional query parameter "release_year" ------------- err = runtime.BindQueryParameter("form", true, false, "release_year", c.Request.URL.Query(), ¶ms.ReleaseYear) @@ -631,130 +868,7 @@ func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { } } - siw.Handler.GetUserTitles(c, userId, params) -} - -// AddUserTitle operation middleware -func (siw *ServerInterfaceWrapper) AddUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - 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 - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.AddUserTitle(c, userId) -} - -// DeleteUserTitle operation middleware -func (siw *ServerInterfaceWrapper) DeleteUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - 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 - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.DeleteUserTitle(c, userId, titleId) -} - -// GetUserTitle operation middleware -func (siw *ServerInterfaceWrapper) GetUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - 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 - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetUserTitle(c, userId, titleId) -} - -// UpdateUserTitle operation middleware -func (siw *ServerInterfaceWrapper) UpdateUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - 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 - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.UpdateUserTitle(c, userId, titleId) + siw.Handler.GetUsersUserIdTitles(c, userId, params) } // GinServerOptions provides options for the Gin server. @@ -785,14 +899,9 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options } router.GET(options.BaseURL+"/titles", wrapper.GetTitles) - router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitle) - router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersId) - router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser) - router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUserTitles) - router.POST(options.BaseURL+"/users/:user_id/titles", wrapper.AddUserTitle) - router.DELETE(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.DeleteUserTitle) - router.GET(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.GetUserTitle) - router.PATCH(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.UpdateUserTitle) + router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitlesTitleId) + router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId) + router.GET(options.BaseURL+"/users/:user_id/titles/", wrapper.GetUsersUserIdTitles) } type GetTitlesRequestObject struct { @@ -841,445 +950,136 @@ func (response GetTitles500Response) VisitGetTitlesResponse(w http.ResponseWrite return nil } -type GetTitleRequestObject struct { +type GetTitlesTitleIdRequestObject struct { TitleId int64 `json:"title_id"` - Params GetTitleParams + Params GetTitlesTitleIdParams } -type GetTitleResponseObject interface { - VisitGetTitleResponse(w http.ResponseWriter) error +type GetTitlesTitleIdResponseObject interface { + VisitGetTitlesTitleIdResponse(w http.ResponseWriter) error } -type GetTitle200JSONResponse Title +type GetTitlesTitleId200JSONResponse Title -func (response GetTitle200JSONResponse) VisitGetTitleResponse(w http.ResponseWriter) error { +func (response GetTitlesTitleId200JSONResponse) VisitGetTitlesTitleIdResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type GetTitle204Response struct { +type GetTitlesTitleId204Response struct { } -func (response GetTitle204Response) VisitGetTitleResponse(w http.ResponseWriter) error { +func (response GetTitlesTitleId204Response) VisitGetTitlesTitleIdResponse(w http.ResponseWriter) error { w.WriteHeader(204) return nil } -type GetTitle400Response struct { +type GetTitlesTitleId400Response struct { } -func (response GetTitle400Response) VisitGetTitleResponse(w http.ResponseWriter) error { +func (response GetTitlesTitleId400Response) VisitGetTitlesTitleIdResponse(w http.ResponseWriter) error { w.WriteHeader(400) return nil } -type GetTitle404Response struct { +type GetTitlesTitleId404Response struct { } -func (response GetTitle404Response) VisitGetTitleResponse(w http.ResponseWriter) error { +func (response GetTitlesTitleId404Response) VisitGetTitlesTitleIdResponse(w http.ResponseWriter) error { w.WriteHeader(404) return nil } -type GetTitle500Response struct { +type GetTitlesTitleId500Response struct { } -func (response GetTitle500Response) VisitGetTitleResponse(w http.ResponseWriter) error { +func (response GetTitlesTitleId500Response) VisitGetTitlesTitleIdResponse(w http.ResponseWriter) error { w.WriteHeader(500) return nil } -type GetUsersIdRequestObject struct { +type GetUsersUserIdRequestObject struct { UserId string `json:"user_id"` - Params GetUsersIdParams + Params GetUsersUserIdParams } -type GetUsersIdResponseObject interface { - VisitGetUsersIdResponse(w http.ResponseWriter) error +type GetUsersUserIdResponseObject interface { + VisitGetUsersUserIdResponse(w http.ResponseWriter) error } -type GetUsersId200JSONResponse User +type GetUsersUserId200JSONResponse User -func (response GetUsersId200JSONResponse) VisitGetUsersIdResponse(w http.ResponseWriter) error { +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 GetUsersId400Response struct { +type GetUsersUserId400Response struct { } -func (response GetUsersId400Response) VisitGetUsersIdResponse(w http.ResponseWriter) error { +func (response GetUsersUserId400Response) VisitGetUsersUserIdResponse(w http.ResponseWriter) error { w.WriteHeader(400) return nil } -type GetUsersId404Response struct { +type GetUsersUserId404Response struct { } -func (response GetUsersId404Response) VisitGetUsersIdResponse(w http.ResponseWriter) error { +func (response GetUsersUserId404Response) VisitGetUsersUserIdResponse(w http.ResponseWriter) error { w.WriteHeader(404) return nil } -type GetUsersId500Response struct { +type GetUsersUserId500Response struct { } -func (response GetUsersId500Response) VisitGetUsersIdResponse(w http.ResponseWriter) error { +func (response GetUsersUserId500Response) VisitGetUsersUserIdResponse(w http.ResponseWriter) error { w.WriteHeader(500) return nil } -type UpdateUserRequestObject struct { - UserId int64 `json:"user_id"` - Body *UpdateUserJSONRequestBody -} - -type UpdateUserResponseObject interface { - VisitUpdateUserResponse(w http.ResponseWriter) error -} - -type UpdateUser200JSONResponse User - -func (response UpdateUser200JSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type UpdateUser400Response struct { -} - -func (response UpdateUser400Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type UpdateUser401Response struct { -} - -func (response UpdateUser401Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type UpdateUser403Response struct { -} - -func (response UpdateUser403Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type UpdateUser404Response struct { -} - -func (response UpdateUser404Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type UpdateUser409Response struct { -} - -func (response UpdateUser409Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(409) - return nil -} - -type UpdateUser422Response struct { -} - -func (response UpdateUser422Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(422) - return nil -} - -type UpdateUser500Response struct { -} - -func (response UpdateUser500Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetUserTitlesRequestObject struct { +type GetUsersUserIdTitlesRequestObject struct { UserId string `json:"user_id"` - Params GetUserTitlesParams + Params GetUsersUserIdTitlesParams } -type GetUserTitlesResponseObject interface { - VisitGetUserTitlesResponse(w http.ResponseWriter) error +type GetUsersUserIdTitlesResponseObject interface { + VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error } -type GetUserTitles200JSONResponse struct { - Cursor CursorObj `json:"cursor"` - Data []UserTitle `json:"data"` -} +type GetUsersUserIdTitles200JSONResponse []UserTitle -func (response GetUserTitles200JSONResponse) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles200JSONResponse) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type GetUserTitles204Response struct { +type GetUsersUserIdTitles204Response struct { } -func (response GetUserTitles204Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles204Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.WriteHeader(204) return nil } -type GetUserTitles400Response struct { +type GetUsersUserIdTitles400Response struct { } -func (response GetUserTitles400Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles400Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.WriteHeader(400) return nil } -type GetUserTitles404Response struct { +type GetUsersUserIdTitles500Response struct { } -func (response GetUserTitles404Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type GetUserTitles500Response struct { -} - -func (response GetUserTitles500Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type AddUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - Body *AddUserTitleJSONRequestBody -} - -type AddUserTitleResponseObject interface { - VisitAddUserTitleResponse(w http.ResponseWriter) error -} - -type AddUserTitle200JSONResponse UserTitleMini - -func (response AddUserTitle200JSONResponse) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type AddUserTitle400Response struct { -} - -func (response AddUserTitle400Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type AddUserTitle401Response struct { -} - -func (response AddUserTitle401Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type AddUserTitle403Response struct { -} - -func (response AddUserTitle403Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type AddUserTitle404Response struct { -} - -func (response AddUserTitle404Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type AddUserTitle409Response struct { -} - -func (response AddUserTitle409Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(409) - return nil -} - -type AddUserTitle500Response struct { -} - -func (response AddUserTitle500Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type DeleteUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` -} - -type DeleteUserTitleResponseObject interface { - VisitDeleteUserTitleResponse(w http.ResponseWriter) error -} - -type DeleteUserTitle200Response struct { -} - -func (response DeleteUserTitle200Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil -} - -type DeleteUserTitle401Response struct { -} - -func (response DeleteUserTitle401Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type DeleteUserTitle403Response struct { -} - -func (response DeleteUserTitle403Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type DeleteUserTitle404Response struct { -} - -func (response DeleteUserTitle404Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type DeleteUserTitle500Response struct { -} - -func (response DeleteUserTitle500Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` -} - -type GetUserTitleResponseObject interface { - VisitGetUserTitleResponse(w http.ResponseWriter) error -} - -type GetUserTitle200JSONResponse UserTitleMini - -func (response GetUserTitle200JSONResponse) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type GetUserTitle204Response struct { -} - -func (response GetUserTitle204Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type GetUserTitle400Response struct { -} - -func (response GetUserTitle400Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetUserTitle404Response struct { -} - -func (response GetUserTitle404Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type GetUserTitle500Response struct { -} - -func (response GetUserTitle500Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type UpdateUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` - Body *UpdateUserTitleJSONRequestBody -} - -type UpdateUserTitleResponseObject interface { - VisitUpdateUserTitleResponse(w http.ResponseWriter) error -} - -type UpdateUserTitle200JSONResponse UserTitleMini - -func (response UpdateUserTitle200JSONResponse) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type UpdateUserTitle400Response struct { -} - -func (response UpdateUserTitle400Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type UpdateUserTitle401Response struct { -} - -func (response UpdateUserTitle401Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type UpdateUserTitle403Response struct { -} - -func (response UpdateUserTitle403Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type UpdateUserTitle404Response struct { -} - -func (response UpdateUserTitle404Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type UpdateUserTitle500Response struct { -} - -func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { +func (response GetUsersUserIdTitles500Response) VisitGetUsersUserIdTitlesResponse(w http.ResponseWriter) error { w.WriteHeader(500) return nil } @@ -1291,28 +1091,13 @@ type StrictServerInterface interface { GetTitles(ctx context.Context, request GetTitlesRequestObject) (GetTitlesResponseObject, error) // Get title description // (GET /titles/{title_id}) - GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error) + GetTitlesTitleId(ctx context.Context, request GetTitlesTitleIdRequestObject) (GetTitlesTitleIdResponseObject, error) // Get user info // (GET /users/{user_id}) - GetUsersId(ctx context.Context, request GetUsersIdRequestObject) (GetUsersIdResponseObject, error) - // Partially update a user account - // (PATCH /users/{user_id}) - UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) + GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) // Get user titles - // (GET /users/{user_id}/titles) - GetUserTitles(ctx context.Context, request GetUserTitlesRequestObject) (GetUserTitlesResponseObject, error) - // Add a title to a user - // (POST /users/{user_id}/titles) - AddUserTitle(ctx context.Context, request AddUserTitleRequestObject) (AddUserTitleResponseObject, error) - // Delete a usertitle - // (DELETE /users/{user_id}/titles/{title_id}) - DeleteUserTitle(ctx context.Context, request DeleteUserTitleRequestObject) (DeleteUserTitleResponseObject, error) - // Get user title - // (GET /users/{user_id}/titles/{title_id}) - GetUserTitle(ctx context.Context, request GetUserTitleRequestObject) (GetUserTitleResponseObject, error) - // Update a usertitle - // (PATCH /users/{user_id}/titles/{title_id}) - UpdateUserTitle(ctx context.Context, request UpdateUserTitleRequestObject) (UpdateUserTitleResponseObject, error) + // (GET /users/{user_id}/titles/) + GetUsersUserIdTitles(ctx context.Context, request GetUsersUserIdTitlesRequestObject) (GetUsersUserIdTitlesResponseObject, error) } type StrictHandlerFunc = strictgin.StrictGinHandlerFunc @@ -1354,18 +1139,18 @@ func (sh *strictHandler) GetTitles(ctx *gin.Context, params GetTitlesParams) { } } -// GetTitle operation middleware -func (sh *strictHandler) GetTitle(ctx *gin.Context, titleId int64, params GetTitleParams) { - var request GetTitleRequestObject +// GetTitlesTitleId operation middleware +func (sh *strictHandler) GetTitlesTitleId(ctx *gin.Context, titleId int64, params GetTitlesTitleIdParams) { + var request GetTitlesTitleIdRequestObject request.TitleId = titleId request.Params = params handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetTitle(ctx, request.(GetTitleRequestObject)) + return sh.ssi.GetTitlesTitleId(ctx, request.(GetTitlesTitleIdRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetTitle") + handler = middleware(handler, "GetTitlesTitleId") } response, err := handler(ctx, request) @@ -1373,8 +1158,8 @@ func (sh *strictHandler) GetTitle(ctx *gin.Context, titleId int64, params GetTit if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetTitleResponseObject); ok { - if err := validResponse.VisitGetTitleResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(GetTitlesTitleIdResponseObject); ok { + if err := validResponse.VisitGetTitlesTitleIdResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { @@ -1382,18 +1167,18 @@ func (sh *strictHandler) GetTitle(ctx *gin.Context, titleId int64, params GetTit } } -// GetUsersId operation middleware -func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetUsersIdParams) { - var request GetUsersIdRequestObject +// 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.GetUsersId(ctx, request.(GetUsersIdRequestObject)) + return sh.ssi.GetUsersUserId(ctx, request.(GetUsersUserIdRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUsersId") + handler = middleware(handler, "GetUsersUserId") } response, err := handler(ctx, request) @@ -1401,8 +1186,8 @@ func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetU if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUsersIdResponseObject); ok { - if err := validResponse.VisitGetUsersIdResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(GetUsersUserIdResponseObject); ok { + if err := validResponse.VisitGetUsersUserIdResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { @@ -1410,53 +1195,18 @@ func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetU } } -// UpdateUser operation middleware -func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64) { - var request UpdateUserRequestObject - - request.UserId = userId - - var body UpdateUserJSONRequestBody - if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return - } - request.Body = &body - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.UpdateUser(ctx, request.(UpdateUserRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UpdateUser") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(UpdateUserResponseObject); ok { - if err := validResponse.VisitUpdateUserResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetUserTitles operation middleware -func (sh *strictHandler) GetUserTitles(ctx *gin.Context, userId string, params GetUserTitlesParams) { - var request GetUserTitlesRequestObject +// GetUsersUserIdTitles operation middleware +func (sh *strictHandler) GetUsersUserIdTitles(ctx *gin.Context, userId string, params GetUsersUserIdTitlesParams) { + var request GetUsersUserIdTitlesRequestObject request.UserId = userId request.Params = params handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUserTitles(ctx, request.(GetUserTitlesRequestObject)) + return sh.ssi.GetUsersUserIdTitles(ctx, request.(GetUsersUserIdTitlesRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUserTitles") + handler = middleware(handler, "GetUsersUserIdTitles") } response, err := handler(ctx, request) @@ -1464,135 +1214,8 @@ func (sh *strictHandler) GetUserTitles(ctx *gin.Context, userId string, params G if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUserTitlesResponseObject); ok { - if err := validResponse.VisitGetUserTitlesResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// AddUserTitle operation middleware -func (sh *strictHandler) AddUserTitle(ctx *gin.Context, userId int64) { - var request AddUserTitleRequestObject - - request.UserId = userId - - var body AddUserTitleJSONRequestBody - if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return - } - request.Body = &body - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.AddUserTitle(ctx, request.(AddUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AddUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(AddUserTitleResponseObject); ok { - if err := validResponse.VisitAddUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// DeleteUserTitle operation middleware -func (sh *strictHandler) DeleteUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request DeleteUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.DeleteUserTitle(ctx, request.(DeleteUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "DeleteUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(DeleteUserTitleResponseObject); ok { - if err := validResponse.VisitDeleteUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetUserTitle operation middleware -func (sh *strictHandler) GetUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request GetUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUserTitle(ctx, request.(GetUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUserTitleResponseObject); ok { - if err := validResponse.VisitGetUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// UpdateUserTitle operation middleware -func (sh *strictHandler) UpdateUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request UpdateUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - var body UpdateUserTitleJSONRequestBody - if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return - } - request.Body = &body - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.UpdateUserTitle(ctx, request.(UpdateUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UpdateUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(UpdateUserTitleResponseObject); ok { - if err := validResponse.VisitUpdateUserTitleResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(GetUsersUserIdTitlesResponseObject); ok { + if err := validResponse.VisitGetUsersUserIdTitlesResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { diff --git a/api/openapi.yaml b/api/openapi.yaml index 08a4d54..281fe82 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -13,11 +13,8 @@ paths: $ref: "./paths/titles-id.yaml" /users/{user_id}: $ref: "./paths/users-id.yaml" - /users/{user_id}/titles: + /users/{user_id}/titles/: $ref: "./paths/users-id-titles.yaml" - /users/{user_id}/titles/{title_id}: - $ref: "./paths/users-id-titles-id.yaml" - components: parameters: $ref: "./parameters/_index.yaml" diff --git a/api/paths/titles-id.yaml b/api/paths/titles-id.yaml index 235743f..01fa504 100644 --- a/api/paths/titles-id.yaml +++ b/api/paths/titles-id.yaml @@ -1,6 +1,5 @@ get: summary: Get title description - operationId: getTitle parameters: - in: path name: title_id diff --git a/api/paths/titles.yaml b/api/paths/titles.yaml index 4288417..e868ed6 100644 --- a/api/paths/titles.yaml +++ b/api/paths/titles.yaml @@ -8,11 +8,6 @@ get: schema: type: boolean default: true - - in: query - name: ext_search - schema: - type: boolean - default: false - in: query name: word schema: @@ -20,12 +15,7 @@ get: - in: query name: status schema: - type: array - items: - $ref: '../schemas/enums/TitleStatus.yaml' - description: List of title statuses to filter - style: form - explode: false + $ref: '../schemas/enums/TitleStatus.yaml' - in: query name: rating schema: diff --git a/api/paths/users-id-titles-id.yaml b/api/paths/users-id-titles-id.yaml deleted file mode 100644 index b4ad884..0000000 --- a/api/paths/users-id-titles-id.yaml +++ /dev/null @@ -1,107 +0,0 @@ -get: - summary: Get user title - operationId: getUserTitle - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: User titles - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '204': - description: No user title found - '400': - description: Request params are not correct - '404': - description: User or title not found - '500': - description: Unknown server error - -patch: - summary: Update a usertitle - description: User updating title list of watched - operationId: updateUserTitle - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - status: - $ref: '../schemas/enums/UserTitleStatus.yaml' - rate: - type: integer - format: int32 - responses: - '200': - description: Title successfully updated - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '400': - description: Invalid request body (missing fields, invalid types, etc.) - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to update title - '404': - description: User or Title not found - '500': - description: Internal server error - -delete: - summary: Delete a usertitle - description: User deleting title from list of watched - operationId: deleteUserTitle - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: Title successfully deleted - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to delete title - '404': - description: User or Title not found - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 75f5461..0cde5af 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -1,19 +1,12 @@ get: summary: Get user titles - operationId: getUserTitles parameters: - $ref: '../parameters/cursor.yaml' - - $ref: "../parameters/title_sort.yaml" - in: path name: user_id required: true schema: type: string - - in: query - name: sort_forward - schema: - type: boolean - default: true - in: query name: word schema: @@ -21,30 +14,16 @@ get: - in: query name: status schema: - type: array - items: - $ref: '../schemas/enums/TitleStatus.yaml' - description: List of title statuses to filter - style: form - explode: false + $ref: '../schemas/enums/TitleStatus.yaml' - in: query name: watch_status schema: - type: array - items: - $ref: '../schemas/enums/UserTitleStatus.yaml' - style: form - explode: false + $ref: '../schemas/enums/UserTitleStatus.yaml' - in: query name: rating schema: type: number format: double - - in: query - name: my_rate - schema: - type: integer - format: int32 - in: query name: release_year schema: @@ -71,73 +50,12 @@ get: content: application/json: schema: - type: object - properties: - data: - type: array - items: - $ref: '../schemas/UserTitle.yaml' - cursor: - $ref: '../schemas/CursorObj.yaml' - required: - - data - - cursor + type: array + items: + $ref: '../schemas/UserTitle.yaml' '204': description: No titles found '400': description: Request params are not correct - '404': - description: User not found '500': description: Unknown server error - -post: - summary: Add a title to a user - description: User adding title to list af watched, status required - operationId: addUserTitle - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: ID of the user to assign the title to - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - title_id - - status - properties: - title_id: - type: integer - format: int64 - status: - $ref: '../schemas/enums/UserTitleStatus.yaml' - rate: - type: integer - format: int32 - responses: - '200': - description: Title successfully added to user - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '400': - description: Invalid request body (missing fields, invalid types, etc.) - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to assign titles to this user - '404': - description: User or Title not found - '409': - description: Conflict — title already assigned to user (if applicable) - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/users-id.yaml b/api/paths/users-id.yaml index fe62e46..0acdb81 100644 --- a/api/paths/users-id.yaml +++ b/api/paths/users-id.yaml @@ -1,6 +1,5 @@ get: summary: Get user info - operationId: getUsersId parameters: - in: path name: user_id @@ -25,79 +24,3 @@ get: description: Request params are not correct '500': description: Unknown server error - -patch: - summary: Partially update a user account - description: | - Update selected user profile fields (excluding password). - Password updates must be done via the dedicated auth-service (`/auth/`). - Fields not provided in the request body remain unchanged. - operationId: updateUser - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: User ID (primary key) - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - avatar_id: - type: integer - format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - example: 42 - mail: - type: string - format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) - example: john.doe.updated@example.com - nickname: - type: string - pattern: '^[a-zA-Z0-9_-]{3,16}$' - description: Username (alphanumeric + `_` or `-`, 3–16 chars) - maxLength: 16 - minLength: 3 - example: john_doe_43 - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Smith - user_desc: - type: string - description: User description / bio - maxLength: 512 - example: Just a curious developer. - additionalProperties: false - description: Only provided fields are updated. Omitted fields remain unchanged. - responses: - '200': - description: User updated successfully. Returns updated user representation (excluding sensitive fields). - content: - application/json: - schema: - $ref: '../schemas/User.yaml' - '400': - description: Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON) - '401': - description: Unauthorized — missing or invalid authentication token - '403': - description: Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights) - '404': - description: User not found - '409': - description: Conflict — e.g., requested `nickname` or `mail` already taken by another user - '422': - description: Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`) - '500': - description: Unknown server error diff --git a/api/schemas/Image.yaml b/api/schemas/Image.yaml index 3fb520b..7226b29 100644 --- a/api/schemas/Image.yaml +++ b/api/schemas/Image.yaml @@ -1,10 +1,9 @@ type: object properties: -# id выпиливаем - id: + id: type: integer format: int64 storage_type: - $ref: './enums/StorageType.yaml' + type: string image_path: type: string diff --git a/api/schemas/Studio.yaml b/api/schemas/Studio.yaml index 26a2adf..35b40a8 100644 --- a/api/schemas/Studio.yaml +++ b/api/schemas/Studio.yaml @@ -3,7 +3,6 @@ required: - id - name properties: -# id не нужен id: type: integer format: int64 diff --git a/api/schemas/Title.yaml b/api/schemas/Title.yaml index 877ee24..7497d1f 100644 --- a/api/schemas/Title.yaml +++ b/api/schemas/Title.yaml @@ -60,3 +60,4 @@ properties: additionalProperties: type: number format: double +additionalProperties: true diff --git a/api/schemas/User.yaml b/api/schemas/User.yaml index 4e53534..8b4d88d 100644 --- a/api/schemas/User.yaml +++ b/api/schemas/User.yaml @@ -5,8 +5,11 @@ properties: format: int64 description: Unique user ID (primary key) example: 1 - image: - $ref: '../schemas/Image.yaml' + avatar_id: + type: integer + format: int64 + description: ID of the user avatar (references images table) + example: null mail: type: string format: email diff --git a/api/schemas/UserTitle.yaml b/api/schemas/UserTitle.yaml index ef619cb..658e350 100644 --- a/api/schemas/UserTitle.yaml +++ b/api/schemas/UserTitle.yaml @@ -7,8 +7,9 @@ properties: user_id: type: integer format: int64 - title: - $ref: ./Title.yaml + title_id: + type: integer + format: int64 status: $ref: ./enums/UserTitleStatus.yaml rate: @@ -20,3 +21,4 @@ properties: ctime: type: string format: date-time +additionalProperties: true diff --git a/api/schemas/UserTitleMini.yaml b/api/schemas/UserTitleMini.yaml deleted file mode 100644 index e1a5a74..0000000 --- a/api/schemas/UserTitleMini.yaml +++ /dev/null @@ -1,23 +0,0 @@ -type: object -required: - - user_id - - title_id - - status -properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: ./enums/UserTitleStatus.yaml - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time diff --git a/api/schemas/_index.yaml b/api/schemas/_index.yaml index d893ced..ac49f37 100644 --- a/api/schemas/_index.yaml +++ b/api/schemas/_index.yaml @@ -1,5 +1,5 @@ CursorObj: - $ref: "./CursorObj.yaml" + $ref: ./CursorObj.yaml TitleSort: $ref: "./TitleSort.yaml" Image: diff --git a/api/schemas/enums/StorageType.yaml b/api/schemas/enums/StorageType.yaml deleted file mode 100644 index 1984a87..0000000 --- a/api/schemas/enums/StorageType.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: string -description: Image storage type -enum: - - s3 - - local \ No newline at end of file diff --git a/api/schemas/updateUser.yaml b/api/schemas/updateUser.yaml deleted file mode 100644 index e1d77af..0000000 --- a/api/schemas/updateUser.yaml +++ /dev/null @@ -1,26 +0,0 @@ -type: object -properties: - avatar_id: - type: integer - format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - example: 42 - mail: - type: string - format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) - example: john.doe.updated@example.com - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Smith - user_desc: - type: string - description: User description / bio - maxLength: 512 - example: Just a curious developer. -additionalProperties: false -description: Only provided fields are updated. Omitted fields remain unchanged. \ No newline at end of file diff --git a/auth/auth.gen.go b/auth/auth.gen.go index 7276545..adb2b06 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -116,8 +116,9 @@ type PostAuthSignInResponseObject interface { } type PostAuthSignIn200JSONResponse struct { - UserId int64 `json:"user_id"` - UserName string `json:"user_name"` + Error *string `json:"error"` + Success *bool `json:"success,omitempty"` + UserId *string `json:"user_id"` } func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { @@ -147,7 +148,9 @@ type PostAuthSignUpResponseObject interface { } type PostAuthSignUp200JSONResponse struct { - UserId int64 `json:"user_id"` + Error *string `json:"error"` + Success *bool `json:"success,omitempty"` + UserId *string `json:"user_id"` } func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http.ResponseWriter) error { diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 239b03b..913c000 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -30,13 +30,16 @@ paths: content: application/json: schema: - required: - - user_id type: object properties: + success: + type: boolean + error: + type: string + nullable: true user_id: - type: integer - format: int64 + type: string + nullable: true /auth/sign-in: post: @@ -56,22 +59,29 @@ paths: type: string format: password responses: - # This one also sets two cookies: access_token and refresh_token "200": description: Sign-in result with JWT + # headers: + # Set-Cookie: + # schema: + # type: array + # items: + # type: string + # explode: true + # style: simple content: application/json: schema: - required: - - user_id - - user_name type: object properties: - user_id: - type: integer - format: int64 - user_name: + success: + type: boolean + error: type: string + nullable: true + user_id: + type: string + nullable: true "401": description: Access denied due to invalid credentials content: diff --git a/deploy/api_gen.ps1 b/deploy/api_gen.ps1 deleted file mode 100644 index c8966b7..0000000 --- a/deploy/api_gen.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -cd ./api -openapi-format .\openapi.yaml --output .\_build\openapi.yaml --yaml -cd .. -oapi-codegen --config=api\oapi-codegen.yaml api\_build\openapi.yaml diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 79ad2f5..1a96253 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -11,34 +11,20 @@ services: - "${POSTGRES_PORT}:5432" volumes: - postgres_data:/var/lib/postgresql - networks: - - nyanimedb-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - rabbitmq: - image: rabbitmq:3-management - container_name: rabbitmq - ports: - - "5672:5672" - - "15672:15672" - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} - volumes: - - rabbitmq_data:/var/lib/rabbitmq - networks: - - nyanimedb-network - healthcheck: - test: ["CMD", "rabbitmqctl", "status"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s + # pgadmin: + # image: dpage/pgadmin4:${PGADMIN_VERSION} + # container_name: pgadmin + # restart: always + # environment: + # PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} + # PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} + # ports: + # - "${PGADMIN_PORT}:80" + # depends_on: + # - postgres + # volumes: + # - pgadmin_data:/var/lib/pgadmin nyanimedb-backend: image: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest @@ -51,23 +37,6 @@ services: - "8080:8080" depends_on: - postgres - - rabbitmq - networks: - - nyanimedb-network - - nyanimedb-auth: - image: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest - container_name: nyanimedb-auth - restart: always - environment: - LOG_LEVEL: ${LOG_LEVEL} - DATABASE_URL: ${DATABASE_URL} - ports: - - "8082:8082" - depends_on: - - postgres - networks: - - nyanimedb-network nyanimedb-frontend: image: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest @@ -77,12 +46,7 @@ services: - "8081:80" depends_on: - nyanimedb-backend - networks: - - nyanimedb-network volumes: postgres_data: - rabbitmq_data: - -networks: - nyanimedb-network: + pgadmin_data: diff --git a/deploy/generate.sh b/deploy/generate.sh index 29587cf..d7d94a2 100644 --- a/deploy/generate.sh +++ b/deploy/generate.sh @@ -1,3 +1,3 @@ -npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios --useUnionTypes +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 \ No newline at end of file diff --git a/go.mod b/go.mod index 6662bc1..bf73121 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module nyanimedb go 1.25.0 require ( - github.com/alexedwards/argon2id v1.0.0 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.0 @@ -37,7 +36,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect - github.com/rabbitmq/amqp091-go v1.10.0 // 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 diff --git a/go.sum b/go.sum index 520a22b..8f46514 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= -github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -72,13 +70,9 @@ 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/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= -github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= 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/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= -github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= 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= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -93,64 +87,26 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS 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= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.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= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 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= diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 261826c..9b9b0d3 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -3,21 +3,22 @@ package handlers import ( "context" "fmt" + "log" "net/http" auth "nyanimedb/auth" sqlc "nyanimedb/sql" "strconv" "time" - "github.com/alexedwards/argon2id" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" - log "github.com/sirupsen/logrus" ) var accessSecret = []byte("my_access_secret_key") var refreshSecret = []byte("my_refresh_secret_key") +var UserDb = make(map[string]string) // TEMP: stores passwords + type Server struct { db *sqlc.Queries } @@ -31,22 +32,6 @@ func parseInt64(s string) (int32, error) { return int32(i), err } -func HashPassword(password string) (string, error) { - params := &argon2id.Params{ - Memory: 64 * 1024, - Iterations: 3, - Parallelism: 2, - SaltLength: 16, - KeyLength: 32, - } - - return argon2id.CreateHash(password, params) -} - -func CheckPassword(password, hash string) (bool, error) { - return argon2id.ComparePasswordAndHash(password, hash) -} - func generateTokens(userID string) (accessToken string, refreshToken string, err error) { accessClaims := jwt.MapClaims{ "user_id": userID, @@ -72,27 +57,19 @@ func generateTokens(userID string) (accessToken string, refreshToken string, err } func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) { - passhash, err := HashPassword(req.Body.Pass) - if err != nil { - log.Errorf("failed to hash password: %v", err) - // TODO: return 500 - } - - user_id, err := s.db.CreateNewUser(context.Background(), sqlc.CreateNewUserParams{ - Passhash: passhash, - Nickname: req.Body.Nickname, - }) - if err != nil { - log.Errorf("failed to create user %s: %v", req.Body.Nickname, err) - // TODO: check err and retyrn 400/500 - } + err := "" + success := true + UserDb[req.Body.Nickname] = req.Body.Pass return auth.PostAuthSignUp200JSONResponse{ - UserId: user_id, + Error: &err, + Success: &success, + UserId: &req.Body.Nickname, }, nil } func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInRequestObject) (auth.PostAuthSignInResponseObject, error) { + // ctx.SetCookie("122") ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) if !ok { log.Print("failed to get gin context") @@ -100,38 +77,28 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque return auth.PostAuthSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") } - user, err := s.db.GetUserByNickname(context.Background(), req.Body.Nickname) - if err != nil { - log.Errorf("failed to get user by nickname %s: %v", req.Body.Nickname, err) - // TODO: return 400/500 - } + err := "" + success := true - ok, err = CheckPassword(req.Body.Pass, user.Passhash) - if err != nil { - log.Errorf("failed to check password for user %s: %v", req.Body.Nickname, err) - // TODO: return 500 - } - if !ok { - err_msg := "invalid credentials" + pass, ok := UserDb[req.Body.Nickname] + if !ok || pass != req.Body.Pass { + e := "invalid credentials" return auth.PostAuthSignIn401JSONResponse{ - Error: &err_msg, + Error: &e, }, nil } - accessToken, refreshToken, err := generateTokens(req.Body.Nickname) - if err != nil { - log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err) - // TODO: return 500 - } + accessToken, refreshToken, _ := generateTokens(req.Body.Nickname) - // TODO: check cookie settings carefully ginCtx.SetSameSite(http.SameSiteStrictMode) - ginCtx.SetCookie("access_token", accessToken, 604800, "/auth", "", false, true) - ginCtx.SetCookie("refresh_token", refreshToken, 604800, "/api", "", false, true) + ginCtx.SetCookie("access_token", accessToken, 604800, "/auth", "", true, true) + ginCtx.SetCookie("refresh_token", refreshToken, 604800, "/api", "", true, true) + // Return access token; refresh token can be returned in response or HttpOnly cookie result := auth.PostAuthSignIn200JSONResponse{ - UserId: user.ID, - UserName: user.Nickname, + Error: &err, + Success: &success, + UserId: &req.Body.Nickname, } return result, nil } diff --git a/modules/auth/main.go b/modules/auth/main.go index 7554f42..c001e8b 100644 --- a/modules/auth/main.go +++ b/modules/auth/main.go @@ -1,9 +1,6 @@ package main import ( - "context" - "fmt" - "os" "time" auth "nyanimedb/auth" @@ -12,22 +9,14 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" ) var AppConfig Config func main() { - // TODO: env args r := gin.Default() - pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL")) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) - os.Exit(1) - } - - var queries *sqlc.Queries = sqlc.New(pool) + var queries *sqlc.Queries = nil server := handlers.NewServer(queries) diff --git a/modules/auth/queries.sql b/modules/auth/queries.sql deleted file mode 100644 index 828d2af..0000000 --- a/modules/auth/queries.sql +++ /dev/null @@ -1,11 +0,0 @@ --- name: GetUserByNickname :one -SELECT * -FROM users -WHERE nickname = sqlc.arg('nickname'); - --- name: CreateNewUser :one -INSERT -INTO users (passhash, nickname) -VALUES (sqlc.arg(passhash), sqlc.arg(nickname)) -RETURNING id; - diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index cad4f0f..b85d022 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -1,165 +1,19 @@ package handlers import ( - "encoding/json" - "fmt" - oapi "nyanimedb/api" - "nyanimedb/modules/backend/rmq" sqlc "nyanimedb/sql" "strconv" ) -type Handler struct { - publisher *rmq.Publisher -} - -func New(publisher *rmq.Publisher) *Handler { - return &Handler{publisher: publisher} -} - type Server struct { - db *sqlc.Queries - publisher *rmq.Publisher - RPCclient *rmq.RPCClient + db *sqlc.Queries } -func NewServer(db *sqlc.Queries, publisher *rmq.Publisher, rpcclient *rmq.RPCClient) *Server { - return &Server{ - db: db, - publisher: publisher, - RPCclient: rpcclient, - } +func NewServer(db *sqlc.Queries) Server { + return Server{db: db} } -func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.StorageType, error) { - if s == nil { - return nil, nil - } - var t oapi.StorageType - switch *s { - case sqlc.StorageTypeTLocal: - t = oapi.Local - case sqlc.StorageTypeTS3: - t = oapi.S3 - default: - return nil, fmt.Errorf("unexpected storage type: %s", *s) - } - return &t, nil -} - -func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) { - - oapi_title := oapi.Title{ - EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAll, - // EpisodesLen: &episodes_lens, - Id: title.ID, - // Poster: &oapi_image, - Rating: title.Rating, - RatingCount: title.RatingCount, - // ReleaseSeason: &release_season, - ReleaseYear: title.ReleaseYear, - // Studio: &oapi_studio, - // Tags: oapi_tag_names, - // TitleNames: title_names, - // TitleStatus: oapi_status, - // AdditionalProperties: - } - - title_names := make(map[string][]string, 0) - err := json.Unmarshal(title.TitleNames, &title_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) - } - oapi_title.TitleNames = title_names - - if len(title.EpisodesLen) > 0 { - episodes_lens := make(map[string]float64, 0) - err = json.Unmarshal(title.EpisodesLen, &episodes_lens) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) - } - oapi_title.EpisodesLen = &episodes_lens - } - - oapi_tag_names := make(oapi.Tags, 0) - err = json.Unmarshal(title.TagNames, &oapi_tag_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) - } - oapi_title.Tags = oapi_tag_names - - var oapi_studio oapi.Studio - if title.StudioName != nil { - oapi_studio.Name = *title.StudioName - } - if title.StudioID != 0 { - oapi_studio.Id = title.StudioID - oapi_studio.Description = title.StudioDesc - if title.StudioIllustID != nil { - oapi_studio.Poster = &oapi.Image{} - oapi_studio.Poster.Id = title.StudioIllustID - oapi_studio.Poster.ImagePath = title.StudioImagePath - - s, err := sql2StorageType(title.StudioStorageType) - if err != nil { - return oapi.Title{}, fmt.Errorf("mapTitle, studio storage type: %v", err) - } - oapi_studio.Poster.StorageType = s - - } - } - oapi_title.Studio = &oapi_studio - - var oapi_image oapi.Image - - if title.PosterID != nil { - oapi_image.Id = title.PosterID - oapi_image.ImagePath = title.TitleImagePath - s, err := sql2StorageType(title.TitleStorageType) - if err != nil { - return oapi.Title{}, fmt.Errorf("mapTitle, title starage type: %v", err) - } - oapi_image.StorageType = s - } - oapi_title.Poster = &oapi_image - - var release_season oapi.ReleaseSeason - if title.ReleaseSeason != nil { - release_season = oapi.ReleaseSeason(*title.ReleaseSeason) - } - oapi_title.ReleaseSeason = &release_season - - oapi_status, err := TitleStatus2oapi(&title.TitleStatus) - if err != nil { - return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) - } - oapi_title.TitleStatus = oapi_status - - return oapi_title, nil -} - -func parseInt64(s string) (int64, error) { +func parseInt64(s string) (int32, error) { i, err := strconv.ParseInt(s, 10, 64) - return i, err -} - -func TitleStatus2Sqlc(s *[]oapi.TitleStatus) ([]sqlc.TitleStatusT, error) { - var sqlc_status []sqlc.TitleStatusT - if s == nil { - return nil, nil - } - for _, t := range *s { - switch t { - case oapi.TitleStatusFinished: - sqlc_status = append(sqlc_status, sqlc.TitleStatusTFinished) - case oapi.TitleStatusOngoing: - sqlc_status = append(sqlc_status, sqlc.TitleStatusTOngoing) - case oapi.TitleStatusPlanned: - sqlc_status = append(sqlc_status, sqlc.TitleStatusTPlanned) - default: - return nil, fmt.Errorf("unexpected tittle status: %s", t) - } - } - return sqlc_status, nil + return int32(i), err } diff --git a/modules/backend/handlers/cursor.go b/modules/backend/handlers/cursor.go deleted file mode 100644 index e3b48b0..0000000 --- a/modules/backend/handlers/cursor.go +++ /dev/null @@ -1,156 +0,0 @@ -package handlers - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "reflect" - "strconv" -) - -// ParseCursorInto parses an opaque base64 cursor and injects values into target struct. -// -// Supported sort types: -// - "id" → sets CursorID (must be *int64) -// - "year" → sets CursorID (*int64) + CursorYear (*int32) -// - "rating" → sets CursorID (*int64) + CursorRating (*float64) -// -// Target struct may have any subset of these fields (e.g. only CursorID). -// Unknown fields are ignored. Missing fields → values are dropped (safe). -// -// Returns error if cursor is invalid or inconsistent with sort_by. -func ParseCursorInto(sortBy, cursorStr string, target any) error { - if cursorStr == "" { - return nil // no cursor → nothing to do - } - - // 1. Decode cursor - payload, err := decodeCursor(cursorStr) - if err != nil { - return err - } - - // 2. Extract ID (required for all types) - id, err := extractInt64(payload, "id") - if err != nil { - return fmt.Errorf("cursor: %v", err) - } - - // 3. Get reflect value of target (must be ptr to struct) - v := reflect.ValueOf(target) - if v.Kind() != reflect.Pointer || v.IsNil() { - return fmt.Errorf("target must be non-nil pointer to struct") - } - v = v.Elem() - if v.Kind() != reflect.Struct { - return fmt.Errorf("target must be pointer to struct") - } - - // 4. Helper: set field if exists and compatible - setField := func(fieldName string, value any) { - f := v.FieldByName(fieldName) - if !f.IsValid() || !f.CanSet() { - return // field not found or unexported - } - ft := f.Type() - vv := reflect.ValueOf(value) - - // Case: field is *T, value is T → wrap in pointer - if ft.Kind() == reflect.Pointer { - elemType := ft.Elem() - if vv.Type().AssignableTo(elemType) { - ptr := reflect.New(elemType) - ptr.Elem().Set(vv) - f.Set(ptr) - } - // nil → leave as zero (nil pointer) - } else if vv.Type().AssignableTo(ft) { - f.Set(vv) - } - // else: type mismatch → silently skip (safe) - } - - // 5. Dispatch by sort type - switch sortBy { - case "id": - setField("CursorID", id) - - case "year": - setField("CursorID", id) - param, err := extractString(payload, "param") - if err != nil { - return fmt.Errorf("cursor year: %w", err) - } - year, err := strconv.Atoi(param) - if err != nil { - return fmt.Errorf("cursor year: param must be integer, got %q", param) - } - setField("CursorYear", int32(year)) // or int, depending on your schema - - case "rating": - setField("CursorID", id) - param, err := extractString(payload, "param") - if err != nil { - return fmt.Errorf("cursor rating: %w", err) - } - rating, err := strconv.ParseFloat(param, 64) - if err != nil { - return fmt.Errorf("cursor rating: param must be float, got %q", param) - } - setField("CursorRating", rating) - - default: - return fmt.Errorf("unsupported sort_by: %q", sortBy) - } - - return nil -} - -// --- helpers --- -func decodeCursor(cursorStr string) (map[string]any, error) { - data, err := base64.RawURLEncoding.DecodeString(cursorStr) - if err != nil { - data, err = base64.StdEncoding.DecodeString(cursorStr) - if err != nil { - return nil, fmt.Errorf("invalid base64 cursor") - } - } - var m map[string]any - if err := json.Unmarshal(data, &m); err != nil { - return nil, fmt.Errorf("invalid cursor JSON: %w", err) - } - return m, nil -} - -func extractInt64(m map[string]any, key string) (int64, error) { - v, ok := m[key] - if !ok { - return 0, fmt.Errorf("missing %q", key) - } - switch x := v.(type) { - case float64: - if x == float64(int64(x)) { - return int64(x), nil - } - case string: - i, err := strconv.ParseInt(x, 10, 64) - if err == nil { - return i, nil - } - case int64, int, int32: - return reflect.ValueOf(x).Int(), nil - } - return 0, fmt.Errorf("%q must be integer", key) -} - -func extractString(m map[string]any, key string) (string, error) { - v, ok := m[key] - if !ok { - return "", fmt.Errorf("missing %q", key) - } - s, ok := v.(string) - if !ok { - return "", fmt.Errorf("%q must be string", key) - } - return s, nil -} diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index 300cc87..f187cc4 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -5,10 +5,7 @@ import ( "encoding/json" "fmt" oapi "nyanimedb/api" - "nyanimedb/modules/backend/rmq" sqlc "nyanimedb/sql" - "strconv" - "time" "github.com/jackc/pgx/v5" log "github.com/sirupsen/logrus" @@ -22,18 +19,18 @@ func Word2Sqlc(s *string) *string { return s } -func TitleStatus2oapi(s *sqlc.TitleStatusT) (*oapi.TitleStatus, error) { +func TitleStatus2Sqlc(s *oapi.TitleStatus) (*sqlc.TitleStatusT, error) { if s == nil { return nil, nil } - var t oapi.TitleStatus + var t sqlc.TitleStatusT switch *s { - case sqlc.TitleStatusTFinished: - t = oapi.TitleStatusFinished - case sqlc.TitleStatusTOngoing: - t = oapi.TitleStatusOngoing - case sqlc.TitleStatusTPlanned: - t = oapi.TitleStatusPlanned + case oapi.TitleStatusFinished: + t = sqlc.TitleStatusTFinished + case oapi.TitleStatusOngoing: + t = sqlc.TitleStatusTOngoing + case oapi.TitleStatusPlanned: + t = sqlc.TitleStatusTPlanned default: return nil, fmt.Errorf("unexpected tittle status: %s", *s) } @@ -83,163 +80,170 @@ func (s Server) GetTagsByTitleId(ctx context.Context, id int64) (oapi.Tags, erro return oapi_tag_names, nil } -// func (s Server) GetImage(ctx context.Context, id int64) (*oapi.Image, error) { +func (s Server) GetImage(ctx context.Context, id int64) (*oapi.Image, error) { -// var oapi_image oapi.Image + var oapi_image oapi.Image -// sqlc_image, err := s.db.GetImageByID(ctx, id) -// if err != nil { -// if err == pgx.ErrNoRows { -// return nil, nil //todo: error reference in db -// } -// return &oapi_image, fmt.Errorf("query GetImageByID: %v", err) -// } + sqlc_image, err := s.db.GetImageByID(ctx, id) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil //todo: error reference in db + } + return &oapi_image, fmt.Errorf("query GetImageByID: %v", err) + } -// //can cast and dont use brain cause all this fields required in image table -// oapi_image.Id = &sqlc_image.ID -// oapi_image.ImagePath = &sqlc_image.ImagePath -// storageTypeStr := string(sqlc_image.StorageType) -// oapi_image.StorageType = string(storageTypeStr) + //can cast and dont use brain cause all this fields required in image table + oapi_image.Id = &sqlc_image.ID + oapi_image.ImagePath = &sqlc_image.ImagePath + storageTypeStr := string(sqlc_image.StorageType) + oapi_image.StorageType = &storageTypeStr -// return &oapi_image, nil -// } + return &oapi_image, nil +} -// func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) { +func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) { -// var oapi_studio oapi.Studio + var oapi_studio oapi.Studio -// sqlc_studio, err := s.db.GetStudioByID(ctx, id) -// if err != nil { -// if err == pgx.ErrNoRows { -// return nil, nil -// } -// return &oapi_studio, fmt.Errorf("query GetStudioByID: %v", err) -// } + sqlc_studio, err := s.db.GetStudioByID(ctx, id) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil + } + return &oapi_studio, fmt.Errorf("query GetStudioByID: %v", err) + } -// oapi_studio.Id = sqlc_studio.ID -// oapi_studio.Name = sqlc_studio.StudioName -// oapi_studio.Description = sqlc_studio.StudioDesc + oapi_studio.Id = sqlc_studio.ID + oapi_studio.Name = sqlc_studio.StudioName + oapi_studio.Description = sqlc_studio.StudioDesc -// if sqlc_studio.IllustID == nil { -// return &oapi_studio, nil -// } -// oapi_illust, err := s.GetImage(ctx, *sqlc_studio.IllustID) -// if err != nil { -// return &oapi_studio, fmt.Errorf("GetImage: %v", err) -// } -// if oapi_illust != nil { -// oapi_studio.Poster = oapi_illust -// } + if sqlc_studio.IllustID == nil { + return &oapi_studio, nil + } + oapi_illust, err := s.GetImage(ctx, *sqlc_studio.IllustID) + if err != nil { + return &oapi_studio, fmt.Errorf("GetImage: %v", err) + } + if oapi_illust != nil { + oapi_studio.Poster = oapi_illust + } -// return &oapi_studio, nil -// } + return &oapi_studio, nil +} -func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject) (oapi.GetTitleResponseObject, error) { +func (s Server) mapTitle(ctx context.Context, title sqlc.Title) (oapi.Title, error) { + + var oapi_title oapi.Title + + title_names := make(map[string][]string, 1) + err := json.Unmarshal(title.TitleNames, &title_names) + if err != nil { + return oapi_title, fmt.Errorf("unmarshal TitleNames: %v", err) + } + + episodes_lens := make(map[string]float64, 1) + err = json.Unmarshal(title.EpisodesLen, &episodes_lens) + if err != nil { + return oapi_title, fmt.Errorf("unmarshal EpisodesLen: %v", err) + } + + oapi_tag_names, err := s.GetTagsByTitleId(ctx, title.ID) + if err != nil { + return oapi_title, fmt.Errorf("GetTagsByTitleId: %v", err) + } + if oapi_tag_names != nil { + oapi_title.Tags = oapi_tag_names + } + + if title.PosterID != nil { + oapi_image, err := s.GetImage(ctx, *title.PosterID) + if err != nil { + return oapi_title, fmt.Errorf("GetImage: %v", err) + } + if oapi_image != nil { + oapi_title.Poster = oapi_image + } + } + + oapi_studio, err := s.GetStudio(ctx, title.StudioID) + if err != nil { + return oapi_title, fmt.Errorf("GetStudio: %v", err) + } + if oapi_studio != nil { + oapi_title.Studio = oapi_studio + } + + if title.ReleaseSeason != nil { + rs := oapi.ReleaseSeason(*title.ReleaseSeason) + oapi_title.ReleaseSeason = &rs + } else { + oapi_title.ReleaseSeason = nil + } + + ts := oapi.TitleStatus(title.TitleStatus) + oapi_title.TitleStatus = &ts + + oapi_title.Id = title.ID + oapi_title.Rating = title.Rating + oapi_title.RatingCount = title.RatingCount + oapi_title.ReleaseYear = title.ReleaseYear + oapi_title.TitleNames = title_names + oapi_title.EpisodesAired = title.EpisodesAired + oapi_title.EpisodesAll = title.EpisodesAll + oapi_title.EpisodesLen = &episodes_lens + + return oapi_title, nil +} + +func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitleIdRequestObject) (oapi.GetTitlesTitleIdResponseObject, error) { var oapi_title oapi.Title sqlc_title, err := s.db.GetTitleByID(ctx, request.TitleId) if err != nil { if err == pgx.ErrNoRows { - return oapi.GetTitle204Response{}, nil + return oapi.GetTitlesTitleId204Response{}, nil } log.Errorf("%v", err) - return oapi.GetTitle500Response{}, nil + return oapi.GetTitlesTitleId500Response{}, nil } - oapi_title, err = s.mapTitle(sqlc_title) + oapi_title, err = s.mapTitle(ctx, sqlc_title) if err != nil { log.Errorf("%v", err) - return oapi.GetTitle500Response{}, nil + return oapi.GetTitlesTitleId500Response{}, nil } - return oapi.GetTitle200JSONResponse(oapi_title), nil + return oapi.GetTitlesTitleId200JSONResponse(oapi_title), nil } func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObject) (oapi.GetTitlesResponseObject, error) { - opai_titles := make([]oapi.Title, 0) - mqreq := rmq.RabbitRequest{ - Timestamp: time.Now(), + cursor := oapi.CursorObj{ + Id: 1, } word := Word2Sqlc(request.Params.Word) - if word != nil { - mqreq.Name = *word + status, err := TitleStatus2Sqlc(request.Params.Status) + if err != nil { + log.Errorf("%v", err) + return oapi.GetTitles400Response{}, err } - season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) if err != nil { log.Errorf("%v", err) return oapi.GetTitles400Response{}, err } - if season != nil { - mqreq.Season = *request.Params.ReleaseSeason - } - - title_statuses, err := TitleStatus2Sqlc(request.Params.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles400Response{}, err - } - if title_statuses != nil { - mqreq.Statuses = *request.Params.Status - } - - if request.Params.ExtSearch != nil && *request.Params.ExtSearch { - - // Структура для ответа (должна совпадать с тем, что шлёт микросервис) - var reply struct { - Status string `json:"status"` - Result string `json:"result"` - Preview string `json:"preview_url"` - } - - // Делаем RPC-вызов — и ЖДЁМ ответа - err := s.RPCclient.Call( - ctx, - "svc.media.process.requests", // ← очередь микросервиса - mqreq, - &reply, - ) - if err != nil { - log.Errorf("RabitMQ: %v", err) - // return oapi.GetTitles500Response{}, err - } - // // Возвращаем результат - // return oapi.ProcessMedia200JSONResponse{ - // Status: reply.Status, - // Result: reply.Result, - // Preview: reply.Preview, - // }, nil - } - - params := sqlc.SearchTitlesParams{ + // param = nil means it will not be used + titles, err := s.db.SearchTitles(ctx, sqlc.SearchTitlesParams{ Word: word, - TitleStatuses: title_statuses, + Status: status, Rating: request.Params.Rating, ReleaseYear: request.Params.ReleaseYear, ReleaseSeason: season, - Forward: true, // default - SortBy: "id", // default + Forward: true, + SortBy: "id", Limit: request.Params.Limit, - } - - if request.Params.SortForward != nil { - params.Forward = *request.Params.SortForward - } - if request.Params.Sort != nil { - params.SortBy = string(*request.Params.Sort) - if request.Params.Cursor != nil { - // here we set CursorYear CursorID CursorRating fields - err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles400Response{}, nil - } - } - } - // param = nil means it will not be used - titles, err := s.db.SearchTitles(ctx, params) + }) if err != nil { log.Errorf("%v", err) return oapi.GetTitles500Response{}, nil @@ -248,58 +252,15 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje return oapi.GetTitles204Response{}, nil } - var new_cursor oapi.CursorObj - for _, title := range titles { - _title := sqlc.GetTitleByIDRow{ - ID: title.ID, - // StudioID: title.StudioID, - PosterID: title.PosterID, - TitleStatus: title.TitleStatus, - Rating: title.Rating, - RatingCount: title.RatingCount, - ReleaseYear: title.ReleaseYear, - ReleaseSeason: title.ReleaseSeason, - Season: title.Season, - EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAll, - // EpisodesLen: title.EpisodesLen, - TitleStorageType: title.TitleStorageType, - TitleImagePath: title.TitleImagePath, - TitleNames: title.TitleNames, - TagNames: title.TagNames, - StudioName: title.StudioName, - // StudioIllustID: title.StudioIllustID, - // StudioDesc: title.StudioDesc, - // StudioStorageType: title.StudioStorageType, - // StudioImagePath: title.StudioImagePath, - } - - // if title.TitleStorageType != nil { - // s := *title.TitleStorageType - // _title.TitleStorageType = string(s) - // } - - t, err := s.mapTitle(_title) + t, err := s.mapTitle(ctx, title) if err != nil { log.Errorf("%v", err) return oapi.GetTitles500Response{}, nil } opai_titles = append(opai_titles, t) - - new_cursor.Id = t.Id - if request.Params.Sort != nil { - switch string(*request.Params.Sort) { - case "year": - tmp := fmt.Sprint(*t.ReleaseYear) - new_cursor.Param = &tmp - case "rating": - tmp := strconv.FormatFloat(*t.Rating, 'f', -1, 64) - new_cursor.Param = &tmp - } - } } - return oapi.GetTitles200JSONResponse{Cursor: new_cursor, Data: opai_titles}, nil + return oapi.GetTitles200JSONResponse{Cursor: cursor, Data: opai_titles}, nil } diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index d6faade..0fa903f 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -2,486 +2,82 @@ package handlers import ( "context" - "errors" - "fmt" oapi "nyanimedb/api" sqlc "nyanimedb/sql" - "strconv" "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" "github.com/oapi-codegen/runtime/types" - log "github.com/sirupsen/logrus" ) -const ( - pgErrDuplicateKey = "23505" -) +// type Server struct { +// db *sqlc.Queries +// } -func mapUser(u sqlc.GetUserByIDRow) (oapi.User, error) { - i := oapi.Image{ - Id: u.AvatarID, - ImagePath: u.ImagePath, - } - s, err := sql2StorageType(u.StorageType) - if err != nil { - return oapi.User{}, fmt.Errorf("mapUser, storage type: %v", err) - } - i.StorageType = s +// 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{ - Image: &i, + AvatarId: u.AvatarID, CreationDate: &u.CreationDate, DispName: u.DispName, Id: &u.ID, - Mail: StringToEmail(u.Mail), + Mail: (*types.Email)(u.Mail), Nickname: u.Nickname, UserDesc: u.UserDesc, - }, nil + } } -func (s Server) GetUsersId(ctx context.Context, req oapi.GetUsersIdRequestObject) (oapi.GetUsersIdResponseObject, error) { +func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdRequestObject) (oapi.GetUsersUserIdResponseObject, error) { userID, err := parseInt64(req.UserId) if err != nil { - return oapi.GetUsersId404Response{}, nil + return oapi.GetUsersUserId404Response{}, nil } - _user, err := s.db.GetUserByID(context.TODO(), userID) + user, err := s.db.GetUserByID(context.TODO(), int64(userID)) if err != nil { if err == pgx.ErrNoRows { - return oapi.GetUsersId404Response{}, nil + return oapi.GetUsersUserId404Response{}, nil } return nil, err } - user, err := mapUser(_user) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUsersId500Response{}, err - } - return oapi.GetUsersId200JSONResponse(user), nil + return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil } -func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time { - if p_date.Valid { - t := p_date.Time - return &t +func (s Server) GetUsersUserIdTitles(ctx context.Context, request oapi.GetUsersUserIdTitlesRequestObject) (oapi.GetUsersUserIdTitlesResponseObject, error) { + + var rate int32 = 9 + var review_id int64 = 3 + time := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC) + + var userTitles = []oapi.UserTitle{ + { + UserId: 101, + TitleId: 2001, + Status: oapi.UserTitleStatusFinished, + Rate: &rate, + Ctime: &time, + }, + { + UserId: 102, + TitleId: 2002, + Status: oapi.UserTitleStatusInProgress, + ReviewId: &review_id, + Ctime: &time, + }, + { + UserId: 103, + TitleId: 2003, + Status: oapi.UserTitleStatusDropped, + Ctime: &time, + }, } - return nil -} - -// func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { -// var sqlc_status SqlcUserStatus -// if s == nil { -// return &sqlc_status, nil -// } -// for _, t := range *s { -// switch t { -// case oapi.UserTitleStatusFinished: -// sqlc_status.finished = "finished" -// case oapi.UserTitleStatusDropped: -// sqlc_status.dropped = "dropped" -// case oapi.UserTitleStatusPlanned: -// sqlc_status.planned = "planned" -// case oapi.UserTitleStatusInProgress: -// sqlc_status.in_progress = "in-progress" -// default: -// return nil, fmt.Errorf("unexpected tittle status: %s", t) -// } -// } -// return &sqlc_status, nil -// } - -func sql2usertitlestatus(s sqlc.UsertitleStatusT) (oapi.UserTitleStatus, error) { - var status oapi.UserTitleStatus - - switch s { - case sqlc.UsertitleStatusTFinished: - status = oapi.UserTitleStatusFinished - case sqlc.UsertitleStatusTDropped: - status = oapi.UserTitleStatusDropped - case sqlc.UsertitleStatusTPlanned: - status = oapi.UserTitleStatusPlanned - case sqlc.UsertitleStatusTInProgress: - status = oapi.UserTitleStatusInProgress - default: - return status, fmt.Errorf("unexpected tittle status: %s", s) - } - - return status, nil -} - -func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) ([]sqlc.UsertitleStatusT, error) { - var sqlc_status []sqlc.UsertitleStatusT - if s == nil { - return nil, nil - } - for _, t := range *s { - switch t { - case oapi.UserTitleStatusFinished: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTFinished) - case oapi.UserTitleStatusInProgress: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTInProgress) - case oapi.UserTitleStatusDropped: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTDropped) - case oapi.UserTitleStatusPlanned: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTPlanned) - default: - return nil, fmt.Errorf("unexpected tittle status: %s", t) - } - } - return sqlc_status, nil -} - -func UserTitleStatus2Sqlc1(s *oapi.UserTitleStatus) (*sqlc.UsertitleStatusT, error) { - var sqlc_status sqlc.UsertitleStatusT = sqlc.UsertitleStatusTFinished - if s == nil { - return &sqlc_status, nil - } - - switch *s { - case oapi.UserTitleStatusFinished: - sqlc_status = sqlc.UsertitleStatusTFinished - case oapi.UserTitleStatusInProgress: - sqlc_status = sqlc.UsertitleStatusTInProgress - case oapi.UserTitleStatusDropped: - sqlc_status = sqlc.UsertitleStatusTDropped - case oapi.UserTitleStatusPlanned: - sqlc_status = sqlc.UsertitleStatusTPlanned - default: - return nil, fmt.Errorf("unexpected tittle status: %s", *s) - } - - return &sqlc_status, nil -} - -func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) { - - oapi_usertitle := oapi.UserTitle{ - Ctime: &t.UserCtime, - Rate: t.UserRate, - ReviewId: t.ReviewID, - // Status: , - // Title: , - UserId: t.UserID, - } - - status, err := sql2usertitlestatus(t.UsertitleStatus) - if err != nil { - return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) - } - oapi_usertitle.Status = status - - _title := sqlc.GetTitleByIDRow{ - ID: t.ID, - // StudioID: title.StudioID, - PosterID: t.PosterID, - TitleStatus: t.TitleStatus, - Rating: t.Rating, - RatingCount: t.RatingCount, - ReleaseYear: t.ReleaseYear, - ReleaseSeason: t.ReleaseSeason, - Season: t.Season, - EpisodesAired: t.EpisodesAired, - EpisodesAll: t.EpisodesAll, - // EpisodesLen: title.EpisodesLen, - TitleStorageType: t.TitleStorageType, - TitleImagePath: t.TitleImagePath, - StudioName: t.StudioName, - TitleNames: t.TitleNames, - TagNames: t.TagNames, - // StudioIllustID: title.StudioIllustID, - // StudioDesc: title.StudioDesc, - // StudioStorageType: title.StudioStorageType, - // StudioImagePath: title.StudioImagePath, - } - - oapi_title, err := s.mapTitle(_title) - if err != nil { - return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) - } - oapi_usertitle.Title = &oapi_title - - return oapi_usertitle, nil -} - -func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesRequestObject) (oapi.GetUserTitlesResponseObject, error) { - - oapi_usertitles := make([]oapi.UserTitle, 0) - - word := Word2Sqlc(request.Params.Word) - - season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err - } - - // var statuses_sort []string - // if request.Params.Status != nil { - // for _, s := range *request.Params.Status { - // ss := string(s) // s type is alias for string - // statuses_sort = append(statuses_sort, ss) - // } - // } - - watch_status, err := UserTitleStatus2Sqlc(request.Params.WatchStatus) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err - } - - title_statuses, err := TitleStatus2Sqlc(request.Params.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err - } - - userID, err := parseInt64(request.UserId) - if err != nil { - log.Errorf("get user titles: %v", err) - return oapi.GetUserTitles404Response{}, err - } - params := sqlc.SearchUserTitlesParams{ - UserID: userID, - Word: word, - TitleStatuses: title_statuses, - UsertitleStatuses: watch_status, - Rating: request.Params.Rating, - Rate: request.Params.MyRate, - ReleaseYear: request.Params.ReleaseYear, - ReleaseSeason: season, - Forward: true, // default - SortBy: "id", // default - Limit: request.Params.Limit, - } - - if request.Params.SortForward != nil { - params.Forward = *request.Params.SortForward - } - if request.Params.Sort != nil { - params.SortBy = string(*request.Params.Sort) - if request.Params.Cursor != nil { - // here we set CursorYear CursorID CursorRating fields - err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, nil - } - } - } - // param = nil means it will not be used - titles, err := s.db.SearchUserTitles(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles500Response{}, nil - } - if len(titles) == 0 { - return oapi.GetUserTitles204Response{}, nil - } - - var new_cursor oapi.CursorObj - - for _, title := range titles { - - t, err := s.mapUsertitle(ctx, title) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles500Response{}, nil - } - oapi_usertitles = append(oapi_usertitles, t) - - new_cursor.Id = t.Title.Id - if request.Params.Sort != nil { - switch string(*request.Params.Sort) { - case "year": - tmp := fmt.Sprint(*t.Title.ReleaseYear) - new_cursor.Param = &tmp - case "rating": - tmp := strconv.FormatFloat(*t.Title.Rating, 'f', -1, 64) // падает - new_cursor.Param = &tmp - } - } - } - - return oapi.GetUserTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil -} - -func EmailToStringPtr(e *types.Email) *string { - if e == nil { - return nil - } - s := string(*e) - return &s -} - -func StringToEmail(e *string) *types.Email { - if e == nil { - return nil - } - s := types.Email(*e) - return &s -} - -// UpdateUser implements oapi.StrictServerInterface. -func (s Server) UpdateUser(ctx context.Context, request oapi.UpdateUserRequestObject) (oapi.UpdateUserResponseObject, error) { - - params := sqlc.UpdateUserParams{ - AvatarID: request.Body.AvatarId, - DispName: request.Body.DispName, - UserDesc: request.Body.UserDesc, - Mail: EmailToStringPtr(request.Body.Mail), - UserID: request.UserId, - } - - user, err := s.db.UpdateUser(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.UpdateUser500Response{}, nil - } - - oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit - // AvatarId: user.AvatarID, - CreationDate: &user.CreationDate, - DispName: user.DispName, - Id: &user.ID, - Mail: StringToEmail(user.Mail), - Nickname: user.Nickname, - UserDesc: user.UserDesc, - } - - return oapi.UpdateUser200JSONResponse(oapi_user), nil -} - -func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleRequestObject) (oapi.AddUserTitleResponseObject, error) { - //TODO: add review if exists - status, err := UserTitleStatus2Sqlc1(&request.Body.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle400Response{}, nil - } - - params := sqlc.InsertUserTitleParams{ - UserID: request.UserId, - TitleID: request.Body.TitleId, - Status: *status, - Rate: request.Body.Rate, - } - - user_title, err := s.db.InsertUserTitle(ctx, params) - if err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) { - // fmt.Println(pgErr.Message) // => syntax error at end of input - // fmt.Println(pgErr.Code) // => 42601 - if pgErr.Code == pgErrDuplicateKey { //duplicate key value - return oapi.AddUserTitle409Response{}, nil - } - } else { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, nil - } - } - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, nil - } - oapi_usertitle := oapi.UserTitleMini{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.AddUserTitle200JSONResponse(oapi_usertitle), nil -} - -// DeleteUserTitle implements oapi.StrictServerInterface. -func (s Server) DeleteUserTitle(ctx context.Context, request oapi.DeleteUserTitleRequestObject) (oapi.DeleteUserTitleResponseObject, error) { - params := sqlc.DeleteUserTitleParams{ - UserID: request.UserId, - TitleID: request.TitleId, - } - _, err := s.db.DeleteUserTitle(ctx, params) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.DeleteUserTitle404Response{}, nil - } - log.Errorf("%v", err) - return oapi.DeleteUserTitle500Response{}, nil - } - return oapi.DeleteUserTitle200Response{}, nil -} - -// UpdateUserTitle implements oapi.StrictServerInterface. -func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitleRequestObject) (oapi.UpdateUserTitleResponseObject, error) { - - status, err := UserTitleStatus2Sqlc1(request.Body.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.UpdateUserTitle400Response{}, nil - } - params := sqlc.UpdateUserTitleParams{ - Status: status, - Rate: request.Body.Rate, - UserID: request.UserId, - TitleID: request.TitleId, - } - - user_title, err := s.db.UpdateUserTitle(ctx, params) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.UpdateUserTitle404Response{}, nil - } - log.Errorf("%v", err) - return oapi.UpdateUserTitle500Response{}, nil - } - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.UpdateUserTitle500Response{}, nil - } - - oapi_usertitle := oapi.UserTitleMini{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.UpdateUserTitle200JSONResponse(oapi_usertitle), nil -} - -func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleRequestObject) (oapi.GetUserTitleResponseObject, error) { - user_title, err := s.db.GetUserTitleByID(ctx, sqlc.GetUserTitleByIDParams{ - TitleID: request.TitleId, - UserID: request.UserId, - }) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.GetUserTitle404Response{}, nil - } else { - log.Errorf("%v", err) - return oapi.GetUserTitle500Response{}, nil - } - } - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitle500Response{}, nil - } - oapi_usertitle := oapi.UserTitleMini{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.GetUserTitle200JSONResponse(oapi_usertitle), nil + + return oapi.GetUsersUserIdTitles200JSONResponse(userTitles), nil } diff --git a/modules/backend/main.go b/modules/backend/main.go index 9f992a5..3ac6603 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "net/http" sqlc "nyanimedb/sql" "os" "reflect" @@ -11,14 +10,11 @@ import ( oapi "nyanimedb/api" handlers "nyanimedb/modules/backend/handlers" - "nyanimedb/modules/backend/rmq" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" "github.com/pelletier/go-toml/v2" - "github.com/rabbitmq/amqp091-go" - log "github.com/sirupsen/logrus" ) var AppConfig Config @@ -47,27 +43,12 @@ func main() { queries := sqlc.New(pool) - // === RabbitMQ setup === - rmqURL := os.Getenv("RABBITMQ_URL") - if rmqURL == "" { - rmqURL = "amqp://guest:guest@rabbitmq:5672/" - } - - rmqConn, err := amqp091.Dial(rmqURL) - if err != nil { - log.Fatalf("Failed to connect to RabbitMQ: %v", err) - } - defer rmqConn.Close() - - publisher := rmq.NewPublisher(rmqConn) - rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second) - - server := handlers.NewServer(queries, publisher, rpcClient) + server := handlers.NewServer(queries) // r.LoadHTMLGlob("templates/*") r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, @@ -79,12 +60,24 @@ func main() { // сюда можно добавить middlewares, если нужно []oapi.StrictMiddlewareFunc{}, )) + // r.GET("/", func(c *gin.Context) { + // c.HTML(http.StatusOK, "index.html", gin.H{ + // "title": "Welcome Page", + // "message": "Hello, Gin with HTML templates!", + // }) + // }) - // Запуск - log.Infof("Server starting on :8080") - if err := r.Run(":8080"); err != nil && err != http.ErrServerClosed { - log.Fatalf("server failed: %v", err) - } + // r.GET("/api", func(c *gin.Context) { + // items := []Item{ + // {ID: 1, Title: "First Item", Description: "This is the description of the first item."}, + // {ID: 2, Title: "Second Item", Description: "This is the description of the second item."}, + // {ID: 3, Title: "Third Item", Description: "This is the description of the third item."}, + // } + + // c.JSON(http.StatusOK, items) + // }) + + r.Run(":8080") } func InitConfig() error { diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index ff41cb1..c05edff 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -9,19 +9,9 @@ VALUES ($1, $2) RETURNING id, storage_type, image_path; -- name: GetUserByID :one -SELECT - t.id as id, - t.avatar_id as avatar_id, - t.mail as mail, - t.nickname as nickname, - t.disp_name as disp_name, - t.user_desc as user_desc, - t.creation_date as creation_date, - i.storage_type as storage_type, - i.image_path as image_path -FROM users as t -LEFT JOIN images as i ON (t.avatar_id = i.id) -WHERE t.id = sqlc.arg('id')::bigint; +SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date +FROM users +WHERE id = $1; -- name: GetStudioByID :one @@ -57,347 +47,218 @@ VALUES ( sqlc.arg('tag_names')::jsonb) RETURNING id, tag_names; --- 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), - mail = COALESCE(sqlc.narg('mail'), mail) -WHERE id = sqlc.arg('user_id') -RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail; +-- -- 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 --- sqlc.struct: TitlesFull -SELECT - t.*, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type as studio_storage_type, - si.image_path as studio_image_path - -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) - -WHERE t.id = sqlc.arg('title_id')::bigint -GROUP BY - t.id, i.id, s.id, si.id; +SELECT * +FROM titles +WHERE id = sqlc.arg('title_id')::bigint; -- name: SearchTitles :many SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name - -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) - -WHERE - CASE - WHEN sqlc.arg('forward')::boolean THEN - -- forward: greater than cursor (next page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year > sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint) - - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating > sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint) - - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id > sqlc.narg('cursor_id')::bigint) - - ELSE true -- fallback - END - - ELSE - -- backward: less than cursor (prev page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year < sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint) - - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating < sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint) - - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id < sqlc.narg('cursor_id')::bigint) - - ELSE true - END + * +FROM titles +WHERE + CASE + WHEN sqlc.narg('word')::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true END - AND ( - CASE - WHEN sqlc.narg('word')::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) - - AND ( - sqlc.narg('title_statuses')::title_status_t[] IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0 - OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[]) - ) - AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) - AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) - AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) - -GROUP BY - t.id, i.id, s.id - + AND (sqlc.narg('status')::title_status_t IS NULL OR title_status = sqlc.narg('status')::title_status_t) + AND (sqlc.narg('rating')::float IS NULL OR rating >= sqlc.narg('rating')::float) + AND (sqlc.narg('release_year')::int IS NULL OR release_year = sqlc.narg('release_year')::int) + AND (sqlc.narg('release_season')::release_season_t IS NULL OR release_season = sqlc.narg('release_season')::release_season_t) ORDER BY - CASE WHEN sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - END - END ASC, - CASE WHEN NOT sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - END - END DESC, - - CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC + -- Основной ключ: выбранное поле + CASE + WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'id' THEN id + WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'year' THEN release_year + WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'rating' THEN rating + -- WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views + END ASC, + CASE + WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'id' THEN id + WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'year' THEN release_year + WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'rating' THEN rating + -- WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views + END DESC, + -- Вторичный ключ: id — только если НЕ сортируем по id + CASE + WHEN sqlc.arg(sort_by)::text != 'id' AND sqlc.arg(forward)::boolean THEN id + END ASC, + CASE + WHEN sqlc.arg(sort_by)::text != 'id' AND NOT sqlc.arg(forward)::boolean THEN id + END DESC LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit +-- OFFSET sqlc.narg('offset')::int; -- name: SearchUserTitles :many -SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - u.user_id as user_id, - u.status as usertitle_status, - u.rate as user_rate, - u.review_id as review_id, - u.ctime as user_ctime, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name - -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) - -WHERE - u.user_id = sqlc.arg('user_id')::bigint - AND - CASE - WHEN sqlc.arg('forward')::boolean THEN - -- forward: greater than cursor (next page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year > sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint) - - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating > sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint) - - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id > sqlc.narg('cursor_id')::bigint) - - ELSE true -- fallback - END - - ELSE - -- backward: less than cursor (prev page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year < sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint) - - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating < sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint) - - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id < sqlc.narg('cursor_id')::bigint) - - ELSE true - END +SELECT + * +FROM usertitles as u +JOIN titles as t ON (u.title_id = t.id) +WHERE + CASE + WHEN sqlc.narg('word')::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(t.title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true END - AND ( - CASE - WHEN sqlc.narg('word')::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) - - AND ( - sqlc.narg('title_statuses')::title_status_t[] IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0 - OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[]) - ) - AND ( - sqlc.narg('usertitle_statuses')::usertitle_status_t[] IS NULL - OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) IS NULL - OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) = 0 - OR u.status = ANY(sqlc.narg('usertitle_statuses')::usertitle_status_t[]) - ) - AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int) + AND (sqlc.narg('status')::title_status_t IS NULL OR t.title_status = sqlc.narg('status')::title_status_t) AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) - -GROUP BY - t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id - -ORDER BY - CASE WHEN sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate - END - END ASC, - CASE WHEN NOT sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate - END - END DESC, - - CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC + AND (sqlc.narg('usertitle_status')::usertitle_status_t IS NULL OR u.usertitle_status = sqlc.narg('usertitle_status')::usertitle_status_t) LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit +-- -- name: ListTitles :many +-- SELECT title_id, title_names, studio_id, poster_id, signal_ids, +-- 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 * FROM reviews WHERE review_id = sqlc.arg('review_id')::bigint; --- name: InsertUserTitle :one -INSERT INTO usertitles (user_id, title_id, status, rate, review_id) -VALUES ( - sqlc.arg('user_id')::bigint, - sqlc.arg('title_id')::bigint, - sqlc.arg('status')::usertitle_status_t, - sqlc.narg('rate')::int, - sqlc.narg('review_id')::bigint -) -RETURNING user_id, title_id, status, rate, review_id, ctime; +-- -- 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: UpdateUserTitle :one --- Fails with sql.ErrNoRows if (user_id, title_id) not found -UPDATE usertitles -SET - status = COALESCE(sqlc.narg('status')::usertitle_status_t, status), - rate = COALESCE(sqlc.narg('rate')::int, rate) -WHERE - user_id = sqlc.arg('user_id') - AND title_id = sqlc.arg('title_id') -RETURNING *; +-- -- 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: DeleteUserTitle :one -DELETE FROM usertitles -WHERE user_id = sqlc.arg('user_id') - AND title_id = sqlc.arg('title_id') -RETURNING *; +-- -- name: DeleteReview :exec +-- DELETE FROM reviews +-- WHERE review_id = $1; --- name: GetUserTitleByID :one -SELECT - ut.* -FROM usertitles as ut -WHERE ut.title_id = sqlc.arg('title_id')::bigint AND ut.user_id = sqlc.arg('user_id')::bigint; \ No newline at end of file +-- 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; \ No newline at end of file diff --git a/modules/backend/rmq/rabbit.go b/modules/backend/rmq/rabbit.go deleted file mode 100644 index 52c1979..0000000 --- a/modules/backend/rmq/rabbit.go +++ /dev/null @@ -1,261 +0,0 @@ -package rmq - -import ( - "context" - "encoding/json" - "fmt" - oapi "nyanimedb/api" - "sync" - "time" - - amqp "github.com/rabbitmq/amqp091-go" -) - -type RabbitRequest struct { - Name string `json:"name"` - Statuses []oapi.TitleStatus `json:"statuses,omitempty"` - Rating float64 `json:"rating,omitempty"` - Year int32 `json:"year,omitempty"` - Season oapi.ReleaseSeason `json:"season,omitempty"` - Timestamp time.Time `json:"timestamp"` -} - -// Publisher — потокобезопасный публикатор с пулом каналов. -type Publisher struct { - conn *amqp.Connection - pool *sync.Pool -} - -// NewPublisher создаёт новый Publisher. -// conn должен быть уже установленным и healthy. -// Рекомендуется передавать durable connection с reconnect-логикой. -func NewPublisher(conn *amqp.Connection) *Publisher { - return &Publisher{ - conn: conn, - pool: &sync.Pool{ - New: func() any { - ch, err := conn.Channel() - if err != nil { - // Паника уместна: невозможность открыть канал — критическая ошибка инициализации - panic(fmt.Errorf("rmqpool: failed to create channel: %w", err)) - } - return ch - }, - }, - } -} - -// Publish публикует сообщение в указанную очередь. -// Очередь объявляется как durable (если не существует). -// Поддерживает context для отмены/таймаута. -func (p *Publisher) Publish( - ctx context.Context, - queueName string, - payload RabbitRequest, - opts ...PublishOption, -) error { - // Применяем опции - options := &publishOptions{ - contentType: "application/json", - deliveryMode: amqp.Persistent, - timestamp: time.Now(), - } - for _, opt := range opts { - opt(options) - } - - // Сериализуем payload - body, err := json.Marshal(payload) - if err != nil { - return fmt.Errorf("rmqpool: failed to marshal payload: %w", err) - } - - // Берём канал из пула - ch := p.getChannel() - if ch == nil { - return fmt.Errorf("rmqpool: channel is nil (connection may be closed)") - } - defer p.returnChannel(ch) - - // Объявляем очередь (idempotent) - q, err := ch.QueueDeclare( - queueName, - true, // durable - false, // auto-delete - false, // exclusive - false, // no-wait - nil, // args - ) - if err != nil { - return fmt.Errorf("rmqpool: failed to declare queue %q: %w", queueName, err) - } - - // Подготавливаем сообщение - msg := amqp.Publishing{ - DeliveryMode: options.deliveryMode, - ContentType: options.contentType, - Timestamp: options.timestamp, - Body: body, - } - - // Публикуем с учётом контекста - done := make(chan error, 1) - go func() { - err := ch.Publish( - "", // exchange (default) - q.Name, // routing key - false, // mandatory - false, // immediate - msg, - ) - done <- err - }() - - select { - case err := <-done: - return err - case <-ctx.Done(): - return ctx.Err() - } -} - -func (p *Publisher) getChannel() *amqp.Channel { - raw := p.pool.Get() - if raw == nil { - ch, _ := p.conn.Channel() - return ch - } - ch := raw.(*amqp.Channel) - if ch.IsClosed() { // ← теперь есть! - ch.Close() // освободить ресурсы - ch, _ = p.conn.Channel() - } - return ch -} - -// returnChannel возвращает канал в пул, если он жив. -func (p *Publisher) returnChannel(ch *amqp.Channel) { - if ch != nil && !ch.IsClosed() { - p.pool.Put(ch) - } -} - -// PublishOption позволяет кастомизировать публикацию. -type PublishOption func(*publishOptions) - -type publishOptions struct { - contentType string - deliveryMode uint8 - timestamp time.Time -} - -// WithContentType устанавливает Content-Type (по умолчанию "application/json"). -func WithContentType(ct string) PublishOption { - return func(o *publishOptions) { o.contentType = ct } -} - -// WithTransient делает сообщение transient (не сохраняется на диск). -// По умолчанию — Persistent. -func WithTransient() PublishOption { - return func(o *publishOptions) { o.deliveryMode = amqp.Transient } -} - -// WithTimestamp устанавливает кастомную метку времени. -func WithTimestamp(ts time.Time) PublishOption { - return func(o *publishOptions) { o.timestamp = ts } -} - -type RPCClient struct { - conn *amqp.Connection - timeout time.Duration -} - -func NewRPCClient(conn *amqp.Connection, timeout time.Duration) *RPCClient { - return &RPCClient{conn: conn, timeout: timeout} -} - -// Call отправляет запрос в очередь и ждёт ответа. -// replyPayload — указатель на структуру, в которую раскодировать ответ (например, &MediaResponse{}). -func (c *RPCClient) Call( - ctx context.Context, - requestQueue string, - request RabbitRequest, - replyPayload any, -) error { - // 1. Создаём временный канал (не из пула!) - ch, err := c.conn.Channel() - if err != nil { - return fmt.Errorf("channel: %w", err) - } - defer ch.Close() - - // 2. Создаём временную очередь для ответов - q, err := ch.QueueDeclare( - "", // auto name - false, // not durable - true, // exclusive - true, // auto-delete - false, - nil, - ) - if err != nil { - return fmt.Errorf("reply queue: %w", err) - } - - // 3. Подписываемся на ответы - msgs, err := ch.Consume( - q.Name, - "", - true, // auto-ack - true, // exclusive - false, - false, - nil, - ) - if err != nil { - return fmt.Errorf("consume: %w", err) - } - - // 4. Готовим correlation ID - corrID := time.Now().UnixNano() - - // 5. Сериализуем запрос - body, err := json.Marshal(request) - if err != nil { - return fmt.Errorf("marshal request: %w", err) - } - - // 6. Публикуем запрос - err = ch.Publish( - "", - requestQueue, - false, - false, - amqp.Publishing{ - ContentType: "application/json", - CorrelationId: fmt.Sprintf("%d", corrID), - ReplyTo: q.Name, - Timestamp: time.Now(), - Body: body, - }, - ) - if err != nil { - return fmt.Errorf("publish: %w", err) - } - - // 7. Ждём ответ с таймаутом - ctx, cancel := context.WithTimeout(ctx, c.timeout) - defer cancel() - - for { - select { - case msg := <-msgs: - if msg.CorrelationId == fmt.Sprintf("%d", corrID) { - return json.Unmarshal(msg.Body, replyPayload) - } - // игнорируем другие сообщения (маловероятно, но возможно) - case <-ctx.Done(): - return ctx.Err() // timeout or cancelled - } - } -} diff --git a/modules/frontend/nginx-default.conf b/modules/frontend/nginx-default.conf index c3a851f..a538968 100644 --- a/modules/frontend/nginx-default.conf +++ b/modules/frontend/nginx-default.conf @@ -19,15 +19,6 @@ server { proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } - location /auth/ { - rewrite ^/auth/(.*)$ /$1 break; - proxy_pass http://nyanimedb-auth:8082/; - 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; diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 95b59e3..5a25313 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -2,38 +2,22 @@ import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import UserPage from "./pages/UserPage/UserPage"; import TitlesPage from "./pages/TitlesPage/TitlesPage"; -import TitlePage from "./pages/TitlePage/TitlePage"; import { LoginPage } from "./pages/LoginPage/LoginPage"; import { Header } from "./components/Header/Header"; const App: React.FC = () => { - const username = localStorage.getItem("username") || undefined; - const userId = localStorage.getItem("userId"); - + const username = "nihonium"; return (
- {/* auth */} - } /> - } /> - {/*} />*/} - - {/* users */} - {/*} />*/} + } /> {/* <-- маршрут для логина */} + } /> {/* <-- можно использовать тот же компонент для регистрации */} } /> - : } - /> - - {/* titles */} } /> - } /> ); }; - export default App; \ No newline at end of file diff --git a/modules/frontend/src/api/core/OpenAPI.ts b/modules/frontend/src/api/core/OpenAPI.ts index 185e5c3..6ce873e 100644 --- a/modules/frontend/src/api/core/OpenAPI.ts +++ b/modules/frontend/src/api/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: '/api/v1', + BASE: 'http://10.1.0.65:8081/api/v1', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/api/index.ts b/modules/frontend/src/api/index.ts index 9013fc7..80ae491 100644 --- a/modules/frontend/src/api/index.ts +++ b/modules/frontend/src/api/index.ts @@ -12,7 +12,6 @@ export type { CursorObj } from './models/CursorObj'; export type { Image } from './models/Image'; export type { ReleaseSeason } from './models/ReleaseSeason'; export type { Review } from './models/Review'; -export type { StorageType } from './models/StorageType'; export type { Studio } from './models/Studio'; export type { Tag } from './models/Tag'; export type { Tags } from './models/Tags'; @@ -22,7 +21,6 @@ export type { TitleSort } from './models/TitleSort'; export type { TitleStatus } from './models/TitleStatus'; export type { User } from './models/User'; export type { UserTitle } from './models/UserTitle'; -export type { UserTitleMini } from './models/UserTitleMini'; export type { UserTitleStatus } from './models/UserTitleStatus'; export { DefaultService } from './services/DefaultService'; diff --git a/modules/frontend/src/api/models/Image.ts b/modules/frontend/src/api/models/Image.ts index 887bf2f..1317db7 100644 --- a/modules/frontend/src/api/models/Image.ts +++ b/modules/frontend/src/api/models/Image.ts @@ -2,10 +2,9 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { StorageType } from './StorageType'; export type Image = { id?: number; - storage_type?: StorageType; + storage_type?: string; image_path?: string; }; diff --git a/modules/frontend/src/api/models/StorageType.ts b/modules/frontend/src/api/models/StorageType.ts deleted file mode 100644 index f6d086b..0000000 --- a/modules/frontend/src/api/models/StorageType.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * Image storage type - */ -export type StorageType = 's3' | 'local'; diff --git a/modules/frontend/src/api/models/Title.ts b/modules/frontend/src/api/models/Title.ts index 9ffdeb6..4da7aa3 100644 --- a/modules/frontend/src/api/models/Title.ts +++ b/modules/frontend/src/api/models/Title.ts @@ -2,30 +2,4 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { Image } from './Image'; -import type { ReleaseSeason } from './ReleaseSeason'; -import type { Studio } from './Studio'; -import type { Tags } from './Tags'; -import type { TitleStatus } from './TitleStatus'; -export type Title = { - /** - * Unique title ID (primary key) - */ - id: number; - /** - * Localized titles. Key = language (ISO 639-1), value = list of names - */ - title_names: Record>; - studio?: Studio; - tags: Tags; - poster?: Image; - title_status?: TitleStatus; - rating?: number; - rating_count?: number; - release_year?: number; - release_season?: ReleaseSeason; - episodes_aired?: number; - episodes_all?: number; - episodes_len?: Record; -}; - +export type Title = Record; diff --git a/modules/frontend/src/api/models/User.ts b/modules/frontend/src/api/models/User.ts index 969023f..cd76dbe 100644 --- a/modules/frontend/src/api/models/User.ts +++ b/modules/frontend/src/api/models/User.ts @@ -2,13 +2,15 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { Image } from './Image'; export type User = { /** * Unique user ID (primary key) */ id?: number; - image?: Image; + /** + * ID of the user avatar (references images table) + */ + avatar_id?: number; /** * User email */ diff --git a/modules/frontend/src/api/models/UserTitle.ts b/modules/frontend/src/api/models/UserTitle.ts index 42b7919..26d5ddc 100644 --- a/modules/frontend/src/api/models/UserTitle.ts +++ b/modules/frontend/src/api/models/UserTitle.ts @@ -2,14 +2,4 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { Title } from './Title'; -import type { UserTitleStatus } from './UserTitleStatus'; -export type UserTitle = { - user_id: number; - title?: Title; - status: UserTitleStatus; - rate?: number; - review_id?: number; - ctime?: string; -}; - +export type UserTitle = Record; diff --git a/modules/frontend/src/api/models/UserTitleMini.ts b/modules/frontend/src/api/models/UserTitleMini.ts deleted file mode 100644 index 2b223ce..0000000 --- a/modules/frontend/src/api/models/UserTitleMini.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { UserTitleStatus } from './UserTitleStatus'; -export type UserTitleMini = { - user_id: number; - title_id: number; - status: UserTitleStatus; - rate?: number; - review_id?: number; - ctime?: string; -}; - diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index 218b461..52321b8 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -9,7 +9,6 @@ import type { TitleSort } from '../models/TitleSort'; import type { TitleStatus } from '../models/TitleStatus'; import type { User } from '../models/User'; import type { UserTitle } from '../models/UserTitle'; -import type { UserTitleMini } from '../models/UserTitleMini'; import type { UserTitleStatus } from '../models/UserTitleStatus'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; @@ -21,7 +20,7 @@ export class DefaultService { * @param sort * @param sortForward * @param word - * @param status List of title statuses to filter + * @param status * @param rating * @param releaseYear * @param releaseSeason @@ -36,7 +35,7 @@ export class DefaultService { sort?: TitleSort, sortForward: boolean = true, word?: string, - status?: Array, + status?: TitleStatus, rating?: number, releaseYear?: number, releaseSeason?: ReleaseSeason, @@ -79,7 +78,7 @@ export class DefaultService { * @returns Title Title description * @throws ApiError */ - public static getTitle( + public static getTitles1( titleId: number, fields: string = 'all', ): CancelablePromise { @@ -106,7 +105,7 @@ export class DefaultService { * @returns User User info * @throws ApiError */ - public static getUsersId( + public static getUsers( userId: string, fields: string = 'all', ): CancelablePromise<User> { @@ -126,112 +125,45 @@ export class DefaultService { }, }); } - /** - * Partially update a user account - * Update selected user profile fields (excluding password). - * Password updates must be done via the dedicated auth-service (`/auth/`). - * Fields not provided in the request body remain unchanged. - * - * @param userId User ID (primary key) - * @param requestBody - * @returns User User updated successfully. Returns updated user representation (excluding sensitive fields). - * @throws ApiError - */ - public static updateUser( - userId: number, - requestBody: { - /** - * ID of the user avatar (references `images.id`); set to `null` to remove avatar - */ - avatar_id?: number | null; - /** - * User email (must be unique and valid) - */ - mail?: string; - /** - * Username (alphanumeric + `_` or `-`, 3–16 chars) - */ - nickname?: string; - /** - * Display name - */ - disp_name?: string; - /** - * User description / bio - */ - user_desc?: string; - }, - ): CancelablePromise<User> { - return __request(OpenAPI, { - method: 'PATCH', - url: '/users/{user_id}', - path: { - 'user_id': userId, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 400: `Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)`, - 401: `Unauthorized — missing or invalid authentication token`, - 403: `Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)`, - 404: `User not found`, - 409: `Conflict — e.g., requested \`nickname\` or \`mail\` already taken by another user`, - 422: `Unprocessable Entity — semantic errors not caught by schema (e.g., invalid \`avatar_id\`)`, - 500: `Unknown server error`, - }, - }); - } /** * Get user titles * @param userId * @param cursor - * @param sort - * @param sortForward * @param word - * @param status List of title statuses to filter + * @param status * @param watchStatus * @param rating - * @param myRate * @param releaseYear * @param releaseSeason * @param limit * @param fields - * @returns any List of user titles + * @returns UserTitle List of user titles * @throws ApiError */ - public static getUserTitles( + public static getUsersTitles( userId: string, cursor?: string, - sort?: TitleSort, - sortForward: boolean = true, word?: string, - status?: Array<TitleStatus>, - watchStatus?: Array<UserTitleStatus>, + status?: TitleStatus, + watchStatus?: UserTitleStatus, rating?: number, - myRate?: number, releaseYear?: number, releaseSeason?: ReleaseSeason, limit: number = 10, fields: string = 'all', - ): CancelablePromise<{ - data: Array<UserTitle>; - cursor: CursorObj; - }> { + ): CancelablePromise<Array<UserTitle>> { return __request(OpenAPI, { method: 'GET', - url: '/users/{user_id}/titles', + url: '/users/{user_id}/titles/', path: { 'user_id': userId, }, query: { 'cursor': cursor, - 'sort': sort, - 'sort_forward': sortForward, 'word': word, 'status': status, 'watch_status': watchStatus, 'rating': rating, - 'my_rate': myRate, 'release_year': releaseYear, 'release_season': releaseSeason, 'limit': limit, @@ -239,130 +171,8 @@ export class DefaultService { }, errors: { 400: `Request params are not correct`, - 404: `User not found`, 500: `Unknown server error`, }, }); } - /** - * Add a title to a user - * User adding title to list af watched, status required - * @param userId ID of the user to assign the title to - * @param requestBody - * @returns UserTitleMini Title successfully added to user - * @throws ApiError - */ - public static addUserTitle( - userId: number, - requestBody: { - title_id: number; - status: UserTitleStatus; - rate?: number; - }, - ): CancelablePromise<UserTitleMini> { - return __request(OpenAPI, { - method: 'POST', - url: '/users/{user_id}/titles', - path: { - 'user_id': userId, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 400: `Invalid request body (missing fields, invalid types, etc.)`, - 401: `Unauthorized — missing or invalid auth token`, - 403: `Forbidden — user not allowed to assign titles to this user`, - 404: `User or Title not found`, - 409: `Conflict — title already assigned to user (if applicable)`, - 500: `Internal server error`, - }, - }); - } - /** - * Get user title - * @param userId - * @param titleId - * @returns UserTitleMini User titles - * @throws ApiError - */ - public static getUserTitle( - userId: number, - titleId: number, - ): CancelablePromise<UserTitleMini> { - return __request(OpenAPI, { - method: 'GET', - url: '/users/{user_id}/titles/{title_id}', - path: { - 'user_id': userId, - 'title_id': titleId, - }, - errors: { - 400: `Request params are not correct`, - 404: `User or title not found`, - 500: `Unknown server error`, - }, - }); - } - /** - * Update a usertitle - * User updating title list of watched - * @param userId - * @param titleId - * @param requestBody - * @returns UserTitleMini Title successfully updated - * @throws ApiError - */ - public static updateUserTitle( - userId: number, - titleId: number, - requestBody: { - status?: UserTitleStatus; - rate?: number; - }, - ): CancelablePromise<UserTitleMini> { - return __request(OpenAPI, { - method: 'PATCH', - url: '/users/{user_id}/titles/{title_id}', - path: { - 'user_id': userId, - 'title_id': titleId, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 400: `Invalid request body (missing fields, invalid types, etc.)`, - 401: `Unauthorized — missing or invalid auth token`, - 403: `Forbidden — user not allowed to update title`, - 404: `User or Title not found`, - 500: `Internal server error`, - }, - }); - } - /** - * Delete a usertitle - * User deleting title from list of watched - * @param userId - * @param titleId - * @returns any Title successfully deleted - * @throws ApiError - */ - public static deleteUserTitle( - userId: number, - titleId: number, - ): CancelablePromise<any> { - return __request(OpenAPI, { - method: 'DELETE', - url: '/users/{user_id}/titles/{title_id}', - path: { - 'user_id': userId, - 'title_id': titleId, - }, - errors: { - 401: `Unauthorized — missing or invalid auth token`, - 403: `Forbidden — user not allowed to delete title`, - 404: `User or Title not found`, - 500: `Internal server error`, - }, - }); - } } diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/auth/core/OpenAPI.ts index 2d0edf8..27bd73f 100644 --- a/modules/frontend/src/auth/core/OpenAPI.ts +++ b/modules/frontend/src/auth/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: '/auth', + BASE: 'http://127.0.0.1:8082', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/auth/services/AuthService.ts b/modules/frontend/src/auth/services/AuthService.ts index 94578d8..bab9c77 100644 --- a/modules/frontend/src/auth/services/AuthService.ts +++ b/modules/frontend/src/auth/services/AuthService.ts @@ -41,9 +41,9 @@ export class AuthService { pass: string; }, ): CancelablePromise<{ + success?: boolean; error?: string | null; user_id?: string | null; - user_name?: string | null; }> { return __request(OpenAPI, { method: 'POST', diff --git a/modules/frontend/src/components/Header/Header.tsx b/modules/frontend/src/components/Header/Header.tsx index 26f1658..98b1295 100644 --- a/modules/frontend/src/components/Header/Header.tsx +++ b/modules/frontend/src/components/Header/Header.tsx @@ -12,7 +12,7 @@ export const Header: React.FC<HeaderProps> = ({ username }) => { const toggleMenu = () => setMenuOpen(!menuOpen); return ( - <header className="w-full bg-white shadow-md sticky top-0 left-0 z-50"> + <header className="w-full bg-white shadow-md fixed top-0 left-0 z-50"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between h-16 items-center"> diff --git a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx deleted file mode 100644 index 0c9c741..0000000 --- a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect, useState } from "react"; -import { DefaultService } from "../../api"; -import type { UserTitleStatus } from "../../api"; -import { - ClockIcon, - CheckCircleIcon, - PlayCircleIcon, - XCircleIcon, -} from "@heroicons/react/24/solid"; - -// Статусы с иконками и подписью -const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ - { status: "planned", icon: <ClockIcon className="w-5 h-5" />, label: "Planned" }, - { status: "finished", icon: <CheckCircleIcon className="w-5 h-5" />, label: "Finished" }, - { status: "in-progress", icon: <PlayCircleIcon className="w-5 h-5" />, label: "In Progress" }, - { status: "dropped", icon: <XCircleIcon className="w-5 h-5" />, label: "Dropped" }, -]; - -export function TitleStatusControls({ titleId }: { titleId: number }) { - const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null); - const [loading, setLoading] = useState(false); - - const userIdStr = localStorage.getItem("userId"); - const userId = userIdStr ? Number(userIdStr) : null; - - // --- Load initial status --- - useEffect(() => { - if (!userId) return; - - DefaultService.getUserTitle(userId, titleId) - .then((res) => setCurrentStatus(res.status)) - .catch(() => setCurrentStatus(null)); // 404 = user title does not exist - }, [titleId, userId]); - - // --- Handle click --- - const handleStatusClick = async (status: UserTitleStatus) => { - if (!userId || loading) return; - - setLoading(true); - - try { - // 1) Если кликнули на текущий статус — DELETE - if (currentStatus === status) { - await DefaultService.deleteUserTitle(userId, titleId); - setCurrentStatus(null); - return; - } - - // 2) Если другой статус — POST или PATCH - if (!currentStatus) { - // ещё нет записи — POST - const added = await DefaultService.addUserTitle(userId, { - title_id: titleId, - status, - }); - setCurrentStatus(added.status); - } else { - // уже есть запись — PATCH - const updated = await DefaultService.updateUserTitle(userId, titleId, { status }); - setCurrentStatus(updated.status); - } - } finally { - setLoading(false); - } - }; - - return ( - <div className="flex gap-2 flex-wrap justify-center mt-2"> - {STATUS_BUTTONS.map(btn => ( - <button - key={btn.status} - onClick={() => handleStatusClick(btn.status)} - disabled={loading} - className={` - px-3 py-1 rounded-md border flex items-center gap-1 transition - ${currentStatus === btn.status - ? "bg-blue-600 text-white border-blue-700" - : "bg-gray-200 text-black border-gray-300 hover:bg-gray-300"} - `} - title={btn.label} - > - {btn.icon} - <span>{btn.label}</span> - </button> - ))} - </div> - ); -} diff --git a/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx b/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx deleted file mode 100644 index ad7d5df..0000000 --- a/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { UserTitle } from "../../api"; - -export function UserTitleCardHorizontal({ title }: { title: UserTitle }) { - return ( - <div style={{ - display: "flex", - gap: 12, - padding: 12, - border: "1px solid #ddd", - borderRadius: 8 - }}> - {title.title?.poster?.image_path && ( - <img src={title.title?.poster.image_path} width={80} /> - )} - <div> - <h3>{title.title?.title_names["en"]}</h3> - <p>{title.title?.release_year} · {title.title?.release_season} · Rating: {title.title?.rating}</p> - <p>Status: {title.title?.title_status}</p> - </div> - </div> - ); -} diff --git a/modules/frontend/src/components/cards/UserTitleCardSquare.tsx b/modules/frontend/src/components/cards/UserTitleCardSquare.tsx deleted file mode 100644 index edcf1d5..0000000 --- a/modules/frontend/src/components/cards/UserTitleCardSquare.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { UserTitle } from "../../api"; - -export function UserTitleCardSquare({ title }: { title: UserTitle }) { - return ( - <div style={{ - width: 160, - border: "1px solid #ddd", - padding: 8, - borderRadius: 8, - textAlign: "center" - }}> - {title.title?.poster?.image_path && ( - <img src={title.title?.poster.image_path} width={140} /> - )} - <div> - <h4>{title.title?.title_names["en"]}</h4> - <h5>{title.status}</h5> - <small>{title.title?.release_year} • {title.title?.rating}</small> - </div> - </div> - ); -} diff --git a/modules/frontend/src/pages/LoginPage/LoginPage.tsx b/modules/frontend/src/pages/LoginPage/LoginPage.tsx index 89ee88c..dcd6965 100644 --- a/modules/frontend/src/pages/LoginPage/LoginPage.tsx +++ b/modules/frontend/src/pages/LoginPage/LoginPage.tsx @@ -18,19 +18,17 @@ export const LoginPage: React.FC = () => { try { if (isLogin) { const res = await AuthService.postAuthSignIn({ nickname, pass: password }); - if (res.user_id && res.user_name) { - // Сохраняем user_id и username в localStorage - localStorage.setItem("userId", res.user_id); - localStorage.setItem("username", res.user_name); - - navigate("/profile"); // редирект на профиль + if (res.success) { + // TODO: сохранить JWT в localStorage/cookie + console.log("Logged in user id:", res.user_id); + navigate("/"); // редирект после успешного входа } else { setError(res.error || "Login failed"); } } else { - // SignUp оставляем без сохранения данных const res = await AuthService.postAuthSignUp({ nickname, pass: password }); - if (res.user_id) { + if (res.success) { + console.log("User signed up with id:", res.user_id); setIsLogin(true); // переключаемся на login после регистрации } else { setError(res.error || "Sign up failed"); diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.module.css b/modules/frontend/src/pages/TitlePage/TitlePage.module.css new file mode 100644 index 0000000..e69de29 diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.tsx b/modules/frontend/src/pages/TitlePage/TitlePage.tsx index 01f9c49..7fe9de7 100644 --- a/modules/frontend/src/pages/TitlePage/TitlePage.tsx +++ b/modules/frontend/src/pages/TitlePage/TitlePage.tsx @@ -1,108 +1,64 @@ -import { useEffect, useState } from "react"; -import { useParams, Link } from "react-router-dom"; -import { DefaultService } from "../../api/services/DefaultService"; -import type { Title } from "../../api"; -import { TitleStatusControls } from "../../components/TitleStatusControls/TitleStatusControls"; +// import React, { useEffect, useState } from "react"; +// import { useParams } from "react-router-dom"; +// import { DefaultService } from "../../api/services/DefaultService"; +// import type { User } from "../../api/models/User"; +// import styles from "./UserPage.module.css"; -export default function TitlePage() { - const params = useParams(); - const titleId = Number(params.id); +// const UserPage: React.FC = () => { +// const { id } = useParams<{ id: string }>(); +// const [user, setUser] = useState<User | null>(null); +// const [loading, setLoading] = useState(true); +// const [error, setError] = useState<string | null>(null); - const [title, setTitle] = useState<Title | null>(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(null); +// useEffect(() => { +// if (!id) return; - // --------------------------- - // LOAD TITLE INFO - // --------------------------- - useEffect(() => { - const fetchTitle = async () => { - setLoading(true); - try { - const data = await DefaultService.getTitle(titleId, "all"); - setTitle(data); - setError(null); - } catch (err: any) { - console.error(err); - setError(err?.message || "Failed to fetch title"); - } finally { - setLoading(false); - } - }; - fetchTitle(); - }, [titleId]); +// const getTitleInfo = async () => { +// try { +// const userInfo = await DefaultService.getTitle(id, "all"); +// setUser(userInfo); +// } catch (err) { +// console.error(err); +// setError("Failed to fetch user info."); +// } finally { +// setLoading(false); +// } +// }; +// getTitleInfo(); +// }, [id]); - const getTagsString = () => - title?.tags?.map(tag => tag.en).filter(Boolean).join(", "); +// 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>; - if (loading) return <div className="mt-20 font-medium text-black">Loading title...</div>; - if (error) return <div className="mt-20 text-red-600 font-medium">{error}</div>; - if (!title) return null; +// 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> - return ( - <div className="w-full min-h-screen bg-gray-50 p-6 flex justify-center"> - <div className="flex flex-col md:flex-row bg-white shadow-lg rounded-xl max-w-4xl w-full p-6 gap-6"> - {/* Poster + status buttons */} - <div className="flex flex-col items-center"> - <img - src={title.poster?.image_path || "/default-poster.png"} - alt={title.title_names?.en?.[0] || "Title poster"} - className="w-48 h-72 object-cover rounded-lg mb-4" - /> +// <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> +// ); +// }; - {/* Status buttons */} - <TitleStatusControls titleId={titleId} /> - </div> - - {/* Title info */} - <div className="flex-1 flex flex-col"> - <h1 className="text-3xl font-bold mb-2"> - {title.title_names?.en?.[0] || "Untitled"} - </h1> - - {title.studio && ( - <p className="text-gray-700 mb-1"> - Studio:{" "} - {title.studio.id ? ( - <Link - to={`/studios/${title.studio.id}`} - className="text-blue-600 hover:underline" - > - {title.studio.name} - </Link> - ) : ( - title.studio.name - )} - </p> - )} - - {title.title_status && <p className="text-gray-700 mb-1">Status: {title.title_status}</p>} - - {title.rating !== undefined && ( - <p className="text-gray-700 mb-1"> - Rating: {title.rating} ({title.rating_count} votes) - </p> - )} - - {title.release_year && ( - <p className="text-gray-700 mb-1"> - Released: {title.release_year} {title.release_season || ""} - </p> - )} - - {title.episodes_aired !== undefined && ( - <p className="text-gray-700 mb-1"> - Episodes: {title.episodes_aired}/{title.episodes_all} - </p> - )} - - {title.tags && title.tags.length > 0 && ( - <p className="text-gray-700 mb-1"> - Tags: {getTagsString()} - </p> - )} - </div> - </div> - </div> - ); -} +// export default UserPage; diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css b/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx index c9911b9..0fec3c8 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx @@ -7,7 +7,6 @@ import { TitleCardSquare } from "../../components/cards/TitleCardSquare"; import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"; import type { CursorObj, Title, TitleSort } from "../../api"; import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; -import { Link } from "react-router-dom"; const PAGE_SIZE = 10; @@ -136,11 +135,11 @@ const handleLoadMore = async () => { hasMore={!!cursor || nextPage.length > 1} loadingMore={loadingMore} onLoadMore={handleLoadMore} - renderItem={(title, layout) => ( - <Link to={`/titles/${title.id}`} key={title.id} className="block"> - {layout === "square" ? <TitleCardSquare title={title} /> : <TitleCardHorizontal title={title} />} - </Link> - )} + renderItem={(title, layout) => + layout === "square" + ? <TitleCardSquare title={title} /> + : <TitleCardHorizontal title={title} /> + } /> {!cursor && nextPage.length == 0 && ( diff --git a/modules/frontend/src/pages/UserPage/UserPage.module.css b/modules/frontend/src/pages/UserPage/UserPage.module.css new file mode 100644 index 0000000..7f350c8 --- /dev/null +++ b/modules/frontend/src/pages/UserPage/UserPage.module.css @@ -0,0 +1,103 @@ +body, +html { + width: 100%; + margin: 0; + background-color: #777; + color: #fff; +} + +html, +body, +#root { + height: 100%; +} + +.header { + width: 100vw; + padding: 30px 40px; + background: #f7f7f7; + display: flex; + align-items: center; + gap: 25px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border-bottom: 1px solid #e5e5e5; + color: #000000; +} + +.avatarWrapper { + width: 120px; + height: 120px; + min-width: 120px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: #ddd; +} + +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatarPlaceholder { + width: 100%; + height: 100%; + border-radius: 50%; + background: #ccc; + font-size: 42px; + font-weight: bold; + color: #555; + display: flex; + align-items: center; + justify-content: center; +} + +.userInfo { + display: flex; + flex-direction: column; +} + +.name { + font-size: 32px; + font-weight: 700; + margin: 0; +} + +.nickname { + font-size: 18px; + color: #666; + margin-top: 6px; +} + +.container { + max-width: 100vw; + width: 100%; + position: absolute; + top: 0%; + /* margin: 25px auto; */ + /* padding: 0 20px; */ +} + +.content { + margin-top: 20px; +} + +.desc { + font-size: 18px; + margin-bottom: 10px; +} + +.created { + font-size: 16px; + color: #888; +} + +.loader, +.error { + text-align: center; + margin-top: 40px; + font-size: 18px; +} diff --git a/modules/frontend/src/pages/UserPage/UserPage.tsx b/modules/frontend/src/pages/UserPage/UserPage.tsx index 7cc0db5..52c5574 100644 --- a/modules/frontend/src/pages/UserPage/UserPage.tsx +++ b/modules/frontend/src/pages/UserPage/UserPage.tsx @@ -1,184 +1,67 @@ -// pages/UserPage/UserPage.tsx -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; // <-- import import { DefaultService } from "../../api/services/DefaultService"; -import { SearchBar } from "../../components/SearchBar/SearchBar"; -import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; -import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; -import { ListView } from "../../components/ListView/ListView"; -import { UserTitleCardSquare } from "../../components/cards/UserTitleCardSquare"; -import { UserTitleCardHorizontal } from "../../components/cards/UserTitleCardHorizontal"; -import type { User, UserTitle, CursorObj, TitleSort } from "../../api"; -import { Link } from "react-router-dom"; - -const PAGE_SIZE = 10; - -type UserPageProps = { - userId?: string; -}; - -export default function UserPage({ userId }: UserPageProps) { - const params = useParams(); - const id = userId || params?.id; +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 [loadingUser, setLoadingUser] = useState(true); - const [errorUser, setErrorUser] = useState<string | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); - // Для списка тайтлов - const [titles, setTitles] = useState<UserTitle[]>([]); - const [nextPage, setNextPage] = useState<UserTitle[]>([]); - const [cursor, setCursor] = useState<CursorObj | null>(null); - const [loadingTitles, setLoadingTitles] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [search, setSearch] = useState(""); - const [sort, setSort] = useState<TitleSort>("id"); - const [sortForward, setSortForward] = useState(true); - const [layout, setLayout] = useState<"square" | "horizontal">("square"); - - // --- Получение данных пользователя --- useEffect(() => { - const fetchUser = async () => { - if (!id) return; - setLoadingUser(true); + if (!id) return; + + const getUserInfo = async () => { try { - const result = await DefaultService.getUsersId(id, "all"); - setUser(result); - setErrorUser(null); - } catch (err: any) { - console.error(err); - setErrorUser(err?.message || "Failed to fetch user data"); - } finally { - setLoadingUser(false); - } - }; - fetchUser(); - }, [id]); - - // --- Получение списка тайтлов пользователя --- - const fetchPage = async (cursorObj: CursorObj | null) => { - if (!id) return { items: [], nextCursor: null }; - const cursorStr = cursorObj - ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") - : ""; - - try { - const result = await DefaultService.getUserTitles( - id, - cursorStr, - sort, - sortForward, - search.trim() || undefined, - undefined, // status фильтр, можно добавить - undefined, // watchStatus - undefined, // rating - undefined, // myRate - undefined, // releaseYear - undefined, // releaseSeason - PAGE_SIZE, - "all" - ); - - if (!result?.data?.length) return { items: [], nextCursor: null }; - - return { items: result.data, nextCursor: result.cursor ?? null }; - } catch (err: any) { - if (err.status === 204) return { items: [], nextCursor: null }; - throw err; - } - }; - - // Инициализация: загружаем сразу две страницы - useEffect(() => { - const initLoad = async () => { - setLoadingTitles(true); - setTitles([]); - setNextPage([]); - setCursor(null); - - const firstPage = await fetchPage(null); - const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null }; - - setTitles(firstPage.items); - setNextPage(secondPage.items); - setCursor(secondPage.nextCursor); - setLoadingTitles(false); - }; - initLoad(); - }, [id, search, sort, sortForward]); - - const handleLoadMore = async () => { - if (nextPage.length === 0) { - setLoadingMore(false); - return; - } - setLoadingMore(true); - - setTitles(prev => [...prev, ...nextPage]); - setNextPage([]); - - if (cursor) { - try { - const next = await fetchPage(cursor); - if (next.items.length > 0) setNextPage(next.items); - setCursor(next.nextCursor); + 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]); - setLoadingMore(false); - }; + 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="w-full min-h-screen bg-gray-50 p-6 flex flex-col items-center"> - - {/* --- Карточка пользователя --- */} - {loadingUser && <div className="mt-10 text-xl font-medium">Loading user...</div>} - {errorUser && <div className="mt-10 text-red-600 font-medium">{errorUser}</div>} - {user && ( - <div className="bg-white shadow-lg rounded-xl p-6 w-full max-w-sm flex flex-col items-center mb-8"> - <img src={user.image?.image_path} alt={user.nickname} className="w-32 h-32 rounded-full object-cover mb-4" /> - <h2 className="text-2xl font-bold mb-2">{user.disp_name || user.nickname}</h2> - {user.mail && <p className="text-gray-600 mb-2">{user.mail}</p>} - {user.user_desc && <p className="text-gray-700 text-center">{user.user_desc}</p>} - {user.creation_date && <p className="text-gray-400 mt-4 text-sm">Registered: {new Date(user.creation_date).toLocaleDateString()}</p>} - </div> - )} - - {/* --- Панель поиска, сортировки и лейаута --- */} - <div className="w-full sm:w-4/5 flex flex-col sm:flex-row gap-4 mb-6 items-center"> - <SearchBar placeholder="Search titles..." search={search} setSearch={setSearch} /> - <LayoutSwitch layout={layout} setLayout={setLayout} /> - <TitlesSortBox sort={sort} setSort={setSort} sortForward={sortForward} setSortForward={setSortForward} /> - </div> - - {/* --- Список тайтлов --- */} - {loadingTitles && <div className="mt-6 font-medium text-black">Loading titles...</div>} - {!loadingTitles && titles.length === 0 && <div className="mt-6 font-medium text-black">No titles found.</div>} - - {titles.length > 0 && ( - <> - <ListView<UserTitle> - items={titles} - layout={layout} - hasMore={!!cursor || nextPage.length > 1} - loadingMore={loadingMore} - onLoadMore={handleLoadMore} - renderItem={(title, layout) => ( - <Link to={`/titles/${title.title?.id}`} key={title.title?.id} className="block"> - {layout === "square" ? <UserTitleCardSquare title={title} /> : <UserTitleCardHorizontal title={title} />} - </Link> - )} - /> - - {!cursor && nextPage.length === 0 && ( - <div className="mt-6 font-medium text-black"> - Результатов больше нет, было найдено {titles.length} тайтлов. + <div className={styles.container}> + <div className={styles.header}> + <div className={styles.avatarWrapper}> + {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.userInfo}> + <h1 className={styles.name}>{user.disp_name || user.nickname}</h1> + <p className={styles.nickname}>@{user.nickname}</p> + {/* <p className={styles.created}> + Joined: {new Date(user.creation_date).toLocaleDateString()} + </p> */} + </div> + + <div className={styles.content}> + {user.user_desc && <p className={styles.desc}>{user.user_desc}</p>} + </div> + </div> </div> ); -} +}; + +export default UserPage; diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index 3499fe2..e6ed628 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -1,3 +1,6 @@ +-- 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'); @@ -21,24 +24,37 @@ CREATE TABLE images ( image_path text UNIQUE NOT NULL ); +CREATE TABLE reviews ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + data text NOT NULL, + rating int CHECK (rating >= 0 AND rating <= 10), + user_id bigint REFERENCES users (id), + title_id bigint REFERENCES titles (id), + created_at timestamptz DEFAULT NOW() +); + +CREATE TABLE review_images ( + PRIMARY KEY (review_id, image_id), + review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, + image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE +); + CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - avatar_id bigint REFERENCES images (id) ON DELETE SET NULL, + 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 UNIQUE NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]{3,}$'), disp_name text, user_desc text, - creation_date timestamptz NOT NULL DEFAULT NOW(), + creation_date timestamptz NOT NULL, last_login timestamptz ); - - CREATE TABLE studios ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, studio_name text NOT NULL UNIQUE, - illust_id bigint REFERENCES images (id) ON DELETE SET NULL, + illust_id bigint REFERENCES images (id), studio_desc text ); @@ -48,7 +64,7 @@ CREATE TABLE titles ( -- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]} title_names jsonb NOT NULL, studio_id bigint NOT NULL REFERENCES studios (id), - poster_id bigint REFERENCES images (id) ON DELETE SET NULL, + 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), @@ -64,36 +80,21 @@ CREATE TABLE titles ( AND episodes_aired <= episodes_all)) ); -CREATE TABLE reviews ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - data text NOT NULL, - rating int CHECK (rating >= 0 AND rating <= 10), - user_id bigint REFERENCES users (id) ON DELETE SET NULL, - title_id bigint REFERENCES titles (id) ON DELETE CASCADE, - created_at timestamptz DEFAULT NOW() -); - -CREATE TABLE review_images ( - PRIMARY KEY (review_id, image_id), - review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, - image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE -); - CREATE TABLE usertitles ( PRIMARY KEY (user_id, title_id), - user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE, - title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, + 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_id bigint REFERENCES reviews (id) ON DELETE SET NULL, - ctime timestamptz NOT NULL DEFAULT now() + review_id bigint REFERENCES reviews (id), + ctime timestamptz -- // TODO: series status ); CREATE TABLE title_tags ( PRIMARY KEY (title_id, tag_id), - title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, - tag_id bigint NOT NULL REFERENCES tags (id) ON DELETE CASCADE + title_id bigint NOT NULL REFERENCES titles (id), + tag_id bigint NOT NULL REFERENCES tags (id) ); CREATE TABLE signals ( @@ -104,17 +105,17 @@ CREATE TABLE signals ( pending boolean NOT NULL ); -CREATE TABLE external_services ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - name text UNIQUE NOT NULL -); - CREATE TABLE external_ids ( user_id bigint NOT NULL REFERENCES users (id), service_id bigint REFERENCES external_services (id), external_id text NOT NULL ); +CREATE TABLE external_services ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name text UNIQUE NOT NULL +); + -- Functions CREATE OR REPLACE FUNCTION update_title_rating() RETURNS TRIGGER AS $$ @@ -168,17 +169,4 @@ EXECUTE FUNCTION update_title_rating(); CREATE TRIGGER trg_notify_new_signal AFTER INSERT ON signals FOR EACH ROW -EXECUTE FUNCTION notify_new_signal(); - -CREATE OR REPLACE FUNCTION set_ctime() -RETURNS TRIGGER AS $$ -BEGIN - NEW.ctime = now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER set_ctime_on_update -BEFORE UPDATE ON usertitles -FOR EACH ROW -EXECUTE FUNCTION set_ctime(); \ No newline at end of file +EXECUTE FUNCTION notify_new_signal(); \ No newline at end of file diff --git a/sql/models.go b/sql/models.go index 842d58c..93cecca 100644 --- a/sql/models.go +++ b/sql/models.go @@ -6,7 +6,6 @@ package sqlc import ( "database/sql/driver" - "encoding/json" "fmt" "time" @@ -224,11 +223,11 @@ type ReviewImage struct { } type Signal struct { - ID int64 `json:"id"` - TitleID *int64 `json:"title_id"` - RawData json.RawMessage `json:"raw_data"` - ProviderID int64 `json:"provider_id"` - Pending bool `json:"pending"` + ID int64 `json:"id"` + TitleID *int64 `json:"title_id"` + RawData []byte `json:"raw_data"` + ProviderID int64 `json:"provider_id"` + Pending bool `json:"pending"` } type Studio struct { @@ -239,13 +238,13 @@ type Studio struct { } type Tag struct { - ID int64 `json:"id"` - TagNames json.RawMessage `json:"tag_names"` + ID int64 `json:"id"` + TagNames []byte `json:"tag_names"` } type Title struct { ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` + TitleNames []byte `json:"title_names"` StudioID int64 `json:"studio_id"` PosterID *int64 `json:"poster_id"` TitleStatus TitleStatusT `json:"title_status"` @@ -277,10 +276,10 @@ type User struct { } type Usertitle struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` - Ctime time.Time `json:"ctime"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` + Status UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + ReviewID *int64 `json:"review_id"` + Ctime pgtype.Timestamptz `json:"ctime"` } diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 1cca986..5a1d13c 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -7,8 +7,9 @@ package sqlc import ( "context" - "encoding/json" "time" + + "github.com/jackc/pgx/v5/pgtype" ) const createImage = `-- name: CreateImage :one @@ -29,51 +30,6 @@ func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Image return i, err } -const createNewUser = `-- name: CreateNewUser :one -INSERT -INTO users (passhash, nickname) -VALUES ($1, $2) -RETURNING id -` - -type CreateNewUserParams struct { - Passhash string `json:"passhash"` - Nickname string `json:"nickname"` -} - -func (q *Queries) CreateNewUser(ctx context.Context, arg CreateNewUserParams) (int64, error) { - row := q.db.QueryRow(ctx, createNewUser, arg.Passhash, arg.Nickname) - var id int64 - err := row.Scan(&id) - return id, err -} - -const deleteUserTitle = `-- name: DeleteUserTitle :one -DELETE FROM usertitles -WHERE user_id = $1 - AND title_id = $2 -RETURNING user_id, title_id, status, rate, review_id, ctime -` - -type DeleteUserTitleParams struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` -} - -func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, deleteUserTitle, arg.UserID, arg.TitleID) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} - const getImageByID = `-- name: GetImageByID :one SELECT id, storage_type, image_path FROM images @@ -89,12 +45,40 @@ func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, erro const getReviewByID = `-- name: GetReviewByID :one + + SELECT id, data, rating, user_id, title_id, created_at FROM reviews WHERE review_id = $1::bigint ` // 100 is default limit +// -- name: ListTitles :many +// SELECT title_id, title_names, studio_id, poster_id, signal_ids, +// +// 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 *; func (q *Queries) GetReviewByID(ctx context.Context, reviewID int64) (Review, error) { row := q.db.QueryRow(ctx, getReviewByID, reviewID) var i Review @@ -128,60 +112,41 @@ func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, er } const getTitleByID = `-- name: GetTitleByID :one -SELECT - t.id, t.title_names, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type as studio_storage_type, - si.image_path as studio_image_path -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) -WHERE t.id = $1::bigint -GROUP BY - t.id, i.id, s.id, si.id + + +SELECT id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len +FROM titles +WHERE id = $1::bigint ` -type GetTitleByIDRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - StudioID int64 `json:"studio_id"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - EpisodesLen []byte `json:"episodes_len"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` - StudioIllustID *int64 `json:"studio_illust_id"` - StudioDesc *string `json:"studio_desc"` - StudioStorageType *StorageTypeT `json:"studio_storage_type"` - StudioImagePath *string `json:"studio_image_path"` -} - -// sqlc.struct: TitlesFull -func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByIDRow, error) { +// -- 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; +func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (Title, error) { row := q.db.QueryRow(ctx, getTitleByID, titleID) - var i GetTitleByIDRow + var i Title err := row.Scan( &i.ID, &i.TitleNames, @@ -196,14 +161,6 @@ func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByID &i.EpisodesAired, &i.EpisodesAll, &i.EpisodesLen, - &i.TitleStorageType, - &i.TitleImagePath, - &i.TagNames, - &i.StudioName, - &i.StudioIllustID, - &i.StudioDesc, - &i.StudioStorageType, - &i.StudioImagePath, ) return i, err } @@ -216,15 +173,15 @@ JOIN title_tags as t ON(t.tag_id = g.id) WHERE t.title_id = $1::bigint ` -func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMessage, error) { +func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([][]byte, error) { rows, err := q.db.Query(ctx, getTitleTags, titleID) if err != nil { return nil, err } defer rows.Close() - items := []json.RawMessage{} + var items [][]byte for rows.Next() { - var tag_names json.RawMessage + var tag_names []byte if err := rows.Scan(&tag_names); err != nil { return nil, err } @@ -237,31 +194,19 @@ func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMe } const getUserByID = `-- name: GetUserByID :one -SELECT - t.id as id, - t.avatar_id as avatar_id, - t.mail as mail, - t.nickname as nickname, - t.disp_name as disp_name, - t.user_desc as user_desc, - t.creation_date as creation_date, - i.storage_type as storage_type, - i.image_path as image_path -FROM users as t -LEFT JOIN images as i ON (t.avatar_id = i.id) -WHERE t.id = $1::bigint +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"` - StorageType *StorageTypeT `json:"storage_type"` - ImagePath *string `json:"image_path"` + 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) { @@ -275,57 +220,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, er &i.DispName, &i.UserDesc, &i.CreationDate, - &i.StorageType, - &i.ImagePath, - ) - return i, err -} - -const getUserByNickname = `-- name: GetUserByNickname :one -SELECT id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date, last_login -FROM users -WHERE nickname = $1 -` - -func (q *Queries) GetUserByNickname(ctx context.Context, nickname string) (User, error) { - row := q.db.QueryRow(ctx, getUserByNickname, nickname) - var i User - err := row.Scan( - &i.ID, - &i.AvatarID, - &i.Passhash, - &i.Mail, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.LastLogin, - ) - return i, err -} - -const getUserTitleByID = `-- name: GetUserTitleByID :one -SELECT - ut.user_id, ut.title_id, ut.status, ut.rate, ut.review_id, ut.ctime -FROM usertitles as ut -WHERE ut.title_id = $1::bigint AND ut.user_id = $2::bigint -` - -type GetUserTitleByIDParams struct { - TitleID int64 `json:"title_id"` - UserID int64 `json:"user_id"` -} - -func (q *Queries) GetUserTitleByID(ctx context.Context, arg GetUserTitleByIDParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, getUserTitleByID, arg.TitleID, arg.UserID) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, ) return i, err } @@ -364,7 +258,7 @@ VALUES ( RETURNING id, tag_names ` -func (q *Queries) InsertTag(ctx context.Context, tagNames json.RawMessage) (Tag, error) { +func (q *Queries) InsertTag(ctx context.Context, tagNames []byte) (Tag, error) { row := q.db.QueryRow(ctx, insertTag, tagNames) var i Tag err := row.Scan(&i.ID, &i.TagNames) @@ -391,229 +285,94 @@ func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams return i, err } -const insertUserTitle = `-- name: InsertUserTitle :one -INSERT INTO usertitles (user_id, title_id, status, rate, review_id) -VALUES ( - $1::bigint, - $2::bigint, - $3::usertitle_status_t, - $4::int, - $5::bigint -) -RETURNING user_id, title_id, status, rate, review_id, ctime -` - -type InsertUserTitleParams struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` -} - -func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, insertUserTitle, - arg.UserID, - arg.TitleID, - arg.Status, - arg.Rate, - arg.ReviewID, - ) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} - const searchTitles = `-- name: SearchTitles :many SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name - -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) - -WHERE - CASE - WHEN $1::boolean THEN - -- forward: greater than cursor (next page) - CASE $2::text - WHEN 'year' THEN - ($3::int IS NULL) OR - (t.release_year > $3::int) OR - (t.release_year = $3::int AND t.id > $4::bigint) - - WHEN 'rating' THEN - ($5::float IS NULL) OR - (t.rating > $5::float) OR - (t.rating = $5::float AND t.id > $4::bigint) - - WHEN 'id' THEN - ($4::bigint IS NULL) OR - (t.id > $4::bigint) - - ELSE true -- fallback - END - - ELSE - -- backward: less than cursor (prev page) - CASE $2::text - WHEN 'year' THEN - ($3::int IS NULL) OR - (t.release_year < $3::int) OR - (t.release_year = $3::int AND t.id < $4::bigint) - - WHEN 'rating' THEN - ($5::float IS NULL) OR - (t.rating < $5::float) OR - (t.rating = $5::float AND t.id < $4::bigint) - - WHEN 'id' THEN - ($4::bigint IS NULL) OR - (t.id < $4::bigint) - - ELSE true - END + id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len +FROM titles +WHERE + CASE + WHEN $1::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array($1::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true END - AND ( - CASE - WHEN $6::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array($6::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) - - AND ( - $7::title_status_t[] IS NULL - OR array_length($7::title_status_t[], 1) IS NULL - OR array_length($7::title_status_t[], 1) = 0 - OR t.title_status = ANY($7::title_status_t[]) - ) - AND ($8::float IS NULL OR t.rating >= $8::float) - AND ($9::int IS NULL OR t.release_year = $9::int) - AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t) - -GROUP BY - t.id, i.id, s.id - + AND ($2::title_status_t IS NULL OR title_status = $2::title_status_t) + AND ($3::float IS NULL OR rating >= $3::float) + AND ($4::int IS NULL OR release_year = $4::int) + AND ($5::release_season_t IS NULL OR release_season = $5::release_season_t) ORDER BY - CASE WHEN $1::boolean THEN - CASE - WHEN $2::text = 'id' THEN t.id - WHEN $2::text = 'year' THEN t.release_year - WHEN $2::text = 'rating' THEN t.rating - END - END ASC, - CASE WHEN NOT $1::boolean THEN - CASE - WHEN $2::text = 'id' THEN t.id - WHEN $2::text = 'year' THEN t.release_year - WHEN $2::text = 'rating' THEN t.rating - END - END DESC, + -- Основной ключ: выбранное поле + CASE + WHEN $6::boolean AND $7::text = 'id' THEN id + WHEN $6::boolean AND $7::text = 'year' THEN release_year + WHEN $6::boolean AND $7::text = 'rating' THEN rating + -- WHEN sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views + END ASC, + CASE + WHEN NOT $6::boolean AND $7::text = 'id' THEN id + WHEN NOT $6::boolean AND $7::text = 'year' THEN release_year + WHEN NOT $6::boolean AND $7::text = 'rating' THEN rating + -- WHEN NOT sqlc.arg(forward)::boolean AND sqlc.arg(sort_by)::text = 'views' THEN views + END DESC, - CASE WHEN $2::text <> 'id' THEN t.id END ASC - -LIMIT COALESCE($11::int, 100) + -- Вторичный ключ: id — только если НЕ сортируем по id + CASE + WHEN $7::text != 'id' AND $6::boolean THEN id + END ASC, + CASE + WHEN $7::text != 'id' AND NOT $6::boolean THEN id + END DESC +LIMIT COALESCE($8::int, 100) ` type SearchTitlesParams struct { - Forward bool `json:"forward"` - SortBy string `json:"sort_by"` - CursorYear *int32 `json:"cursor_year"` - CursorID *int64 `json:"cursor_id"` - CursorRating *float64 `json:"cursor_rating"` Word *string `json:"word"` - TitleStatuses []TitleStatusT `json:"title_statuses"` + Status *TitleStatusT `json:"status"` Rating *float64 `json:"rating"` ReleaseYear *int32 `json:"release_year"` ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Forward bool `json:"forward"` + SortBy string `json:"sort_by"` Limit *int32 `json:"limit"` } -type SearchTitlesRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` -} - -func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]SearchTitlesRow, error) { +func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]Title, error) { rows, err := q.db.Query(ctx, searchTitles, - arg.Forward, - arg.SortBy, - arg.CursorYear, - arg.CursorID, - arg.CursorRating, arg.Word, - arg.TitleStatuses, + arg.Status, arg.Rating, arg.ReleaseYear, arg.ReleaseSeason, + arg.Forward, + arg.SortBy, arg.Limit, ) if err != nil { return nil, err } defer rows.Close() - items := []SearchTitlesRow{} + var items []Title for rows.Next() { - var i SearchTitlesRow + var i Title if err := rows.Scan( &i.ID, &i.TitleNames, + &i.StudioID, &i.PosterID, &i.TitleStatus, &i.Rating, @@ -623,10 +382,7 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S &i.Season, &i.EpisodesAired, &i.EpisodesAll, - &i.TitleStorageType, - &i.TitleImagePath, - &i.TagNames, - &i.StudioName, + &i.EpisodesLen, ); err != nil { return nil, err } @@ -640,217 +396,102 @@ func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]S const searchUserTitles = `-- name: SearchUserTitles :many -SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - u.user_id as user_id, - u.status as usertitle_status, - u.rate as user_rate, - u.review_id as review_id, - u.ctime as user_ctime, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name - -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) - -WHERE - u.user_id = $1::bigint - AND - CASE - WHEN $2::boolean THEN - -- forward: greater than cursor (next page) - CASE $3::text - WHEN 'year' THEN - ($4::int IS NULL) OR - (t.release_year > $4::int) OR - (t.release_year = $4::int AND t.id > $5::bigint) - - WHEN 'rating' THEN - ($6::float IS NULL) OR - (t.rating > $6::float) OR - (t.rating = $6::float AND t.id > $5::bigint) - - WHEN 'id' THEN - ($5::bigint IS NULL) OR - (t.id > $5::bigint) - - ELSE true -- fallback - END - - ELSE - -- backward: less than cursor (prev page) - CASE $3::text - WHEN 'year' THEN - ($4::int IS NULL) OR - (t.release_year < $4::int) OR - (t.release_year = $4::int AND t.id < $5::bigint) - - WHEN 'rating' THEN - ($6::float IS NULL) OR - (t.rating < $6::float) OR - (t.rating = $6::float AND t.id < $5::bigint) - - WHEN 'id' THEN - ($5::bigint IS NULL) OR - (t.id < $5::bigint) - - ELSE true - END +SELECT + user_id, title_id, status, rate, review_id, ctime, id, title_names, studio_id, poster_id, title_status, rating, rating_count, release_year, release_season, season, episodes_aired, episodes_all, episodes_len +FROM usertitles as u +JOIN titles as t ON (u.title_id = t.id) +WHERE + CASE + WHEN $1::text IS NOT NULL THEN + ( + SELECT bool_and( + EXISTS ( + SELECT 1 + FROM jsonb_each_text(t.title_names) AS t(key, val) + WHERE val ILIKE pattern + ) + ) + FROM unnest( + ARRAY( + SELECT '%' || trim(w) || '%' + FROM unnest(string_to_array($1::text, ' ')) AS w + WHERE trim(w) <> '' + ) + ) AS pattern + ) + ELSE true END - AND ( - CASE - WHEN $7::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array($7::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) + AND ($2::title_status_t IS NULL OR t.title_status = $2::title_status_t) + AND ($3::float IS NULL OR t.rating >= $3::float) + AND ($4::int IS NULL OR t.release_year = $4::int) + AND ($5::release_season_t IS NULL OR t.release_season = $5::release_season_t) + AND ($6::usertitle_status_t IS NULL OR u.usertitle_status = $6::usertitle_status_t) - AND ( - $8::title_status_t[] IS NULL - OR array_length($8::title_status_t[], 1) IS NULL - OR array_length($8::title_status_t[], 1) = 0 - OR t.title_status = ANY($8::title_status_t[]) - ) - AND ( - $9::usertitle_status_t[] IS NULL - OR array_length($9::usertitle_status_t[], 1) IS NULL - OR array_length($9::usertitle_status_t[], 1) = 0 - OR u.status = ANY($9::usertitle_status_t[]) - ) - AND ($10::int IS NULL OR u.rate >= $10::int) - AND ($11::float IS NULL OR t.rating >= $11::float) - AND ($12::int IS NULL OR t.release_year = $12::int) - AND ($13::release_season_t IS NULL OR t.release_season = $13::release_season_t) - -GROUP BY - t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id - -ORDER BY - CASE WHEN $2::boolean THEN - CASE - WHEN $3::text = 'id' THEN t.id - WHEN $3::text = 'year' THEN t.release_year - WHEN $3::text = 'rating' THEN t.rating - WHEN $3::text = 'rate' THEN u.rate - END - END ASC, - CASE WHEN NOT $2::boolean THEN - CASE - WHEN $3::text = 'id' THEN t.id - WHEN $3::text = 'year' THEN t.release_year - WHEN $3::text = 'rating' THEN t.rating - WHEN $3::text = 'rate' THEN u.rate - END - END DESC, - - CASE WHEN $3::text <> 'id' THEN t.id END ASC - -LIMIT COALESCE($14::int, 100) +LIMIT COALESCE($7::int, 100) ` type SearchUserTitlesParams struct { - UserID int64 `json:"user_id"` - Forward bool `json:"forward"` - SortBy string `json:"sort_by"` - CursorYear *int32 `json:"cursor_year"` - CursorID *int64 `json:"cursor_id"` - CursorRating *float64 `json:"cursor_rating"` - Word *string `json:"word"` - TitleStatuses []TitleStatusT `json:"title_statuses"` - UsertitleStatuses []UsertitleStatusT `json:"usertitle_statuses"` - Rate *int32 `json:"rate"` - Rating *float64 `json:"rating"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Limit *int32 `json:"limit"` + Word *string `json:"word"` + Status *TitleStatusT `json:"status"` + Rating *float64 `json:"rating"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + UsertitleStatus NullUsertitleStatusT `json:"usertitle_status"` + Limit *int32 `json:"limit"` } type SearchUserTitlesRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - UserID int64 `json:"user_id"` - UsertitleStatus UsertitleStatusT `json:"usertitle_status"` - UserRate *int32 `json:"user_rate"` - ReviewID *int64 `json:"review_id"` - UserCtime time.Time `json:"user_ctime"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` + Status UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + ReviewID *int64 `json:"review_id"` + Ctime pgtype.Timestamptz `json:"ctime"` + ID int64 `json:"id"` + TitleNames []byte `json:"title_names"` + StudioID int64 `json:"studio_id"` + PosterID *int64 `json:"poster_id"` + TitleStatus TitleStatusT `json:"title_status"` + Rating *float64 `json:"rating"` + RatingCount *int32 `json:"rating_count"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason *ReleaseSeasonT `json:"release_season"` + Season *int32 `json:"season"` + EpisodesAired *int32 `json:"episodes_aired"` + EpisodesAll *int32 `json:"episodes_all"` + EpisodesLen []byte `json:"episodes_len"` } // 100 is default limit +// OFFSET sqlc.narg('offset')::int; func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) { rows, err := q.db.Query(ctx, searchUserTitles, - arg.UserID, - arg.Forward, - arg.SortBy, - arg.CursorYear, - arg.CursorID, - arg.CursorRating, arg.Word, - arg.TitleStatuses, - arg.UsertitleStatuses, - arg.Rate, + arg.Status, arg.Rating, arg.ReleaseYear, arg.ReleaseSeason, + arg.UsertitleStatus, arg.Limit, ) if err != nil { return nil, err } defer rows.Close() - items := []SearchUserTitlesRow{} + var items []SearchUserTitlesRow for rows.Next() { var i SearchUserTitlesRow if err := rows.Scan( + &i.UserID, + &i.TitleID, + &i.Status, + &i.Rate, + &i.ReviewID, + &i.Ctime, &i.ID, &i.TitleNames, + &i.StudioID, &i.PosterID, &i.TitleStatus, &i.Rating, @@ -860,15 +501,7 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara &i.Season, &i.EpisodesAired, &i.EpisodesAll, - &i.UserID, - &i.UsertitleStatus, - &i.UserRate, - &i.ReviewID, - &i.UserCtime, - &i.TitleStorageType, - &i.TitleImagePath, - &i.TagNames, - &i.StudioName, + &i.EpisodesLen, ); err != nil { return nil, err } @@ -879,91 +512,3 @@ func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesPara } return items, nil } - -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), - mail = COALESCE($4, mail) -WHERE id = $5 -RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail -` - -type UpdateUserParams struct { - AvatarID *int64 `json:"avatar_id"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - Mail *string `json:"mail"` - UserID int64 `json:"user_id"` -} - -type UpdateUserRow struct { - ID int64 `json:"id"` - AvatarID *int64 `json:"avatar_id"` - Nickname string `json:"nickname"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - CreationDate time.Time `json:"creation_date"` - Mail *string `json:"mail"` -} - -func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) { - row := q.db.QueryRow(ctx, updateUser, - arg.AvatarID, - arg.DispName, - arg.UserDesc, - arg.Mail, - arg.UserID, - ) - var i UpdateUserRow - err := row.Scan( - &i.ID, - &i.AvatarID, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.Mail, - ) - return i, err -} - -const updateUserTitle = `-- name: UpdateUserTitle :one -UPDATE usertitles -SET - status = COALESCE($1::usertitle_status_t, status), - rate = COALESCE($2::int, rate) -WHERE - user_id = $3 - AND title_id = $4 -RETURNING user_id, title_id, status, rate, review_id, ctime -` - -type UpdateUserTitleParams struct { - Status *UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` -} - -// Fails with sql.ErrNoRows if (user_id, title_id) not found -func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, updateUserTitle, - arg.Status, - arg.Rate, - arg.UserID, - arg.TitleID, - ) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 904abaf..f74d2ad 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -3,7 +3,6 @@ sql: - engine: "postgresql" queries: - "../modules/backend/queries.sql" - - "../modules/auth/queries.sql" schema: "migrations" gen: go: @@ -13,20 +12,7 @@ sql: sql_driver: "github.com/jackc/pgx/v5" emit_json_tags: true emit_pointers_for_null_types: true - emit_empty_slices: true #slices returned by :many queries will be empty instead of nil overrides: - - db_type: "usertitle_status_t" - nullable: true - go_type: - type: "UsertitleStatusT" - pointer: true - - db_type: "storage_type_t" - nullable: true - go_type: - type: "StorageTypeT" - pointer: true - - db_type: "jsonb" - go_type: "encoding/json.RawMessage" - db_type: "uuid" nullable: false go_type: