From e0a68d7d0f9c4cd834ace7bc721a9d6dbe51aaba Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Thu, 27 Nov 2025 05:48:13 +0300 Subject: [PATCH 1/3] feat: delete usertitle described --- api/_build/openapi.yaml | 423 ++++++++++++++++++--------------- api/api.gen.go | 290 ++++++++++++++++++++-- api/openapi.yaml | 1 - api/paths/users-id-titles.yaml | 33 ++- api/schemas/UserTitleMini.yaml | 3 +- deploy/api_gen.ps1 | 4 + 6 files changed, 526 insertions(+), 228 deletions(-) create mode 100644 deploy/api_gen.ps1 diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index e7482c1..403a45c 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -11,52 +11,52 @@ paths: parameters: - $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/title_sort' - - in: query - name: sort_forward + - name: sort_forward + in: query schema: type: boolean default: true - - in: query - name: word + - name: word + in: query schema: type: string - - in: query - name: status + - name: status + in: query + description: List of title statuses to filter schema: type: array items: $ref: '#/components/schemas/TitleStatus' - description: List of title statuses to filter - style: form explode: false - - in: query - name: rating + style: form + - name: rating + in: query schema: type: number format: double - - in: query - name: release_year + - name: release_year + in: query schema: type: integer format: int32 - - in: query - name: release_season + - name: release_season + in: query schema: $ref: '#/components/schemas/ReleaseSeason' - - in: query - name: limit + - name: limit + in: query schema: type: integer format: int32 default: 10 - - in: query - name: offset + - name: offset + in: query schema: type: integer format: int32 default: 0 - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -69,10 +69,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: @@ -88,14 +88,14 @@ paths: get: summary: Get title description parameters: - - in: path - name: title_id + - name: title_id + in: path required: true schema: type: integer format: int64 - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -118,13 +118,13 @@ paths: get: summary: Get user info parameters: - - in: path - name: user_id + - name: user_id + in: path required: true schema: type: string - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -142,59 +142,59 @@ paths: '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. - operationId: updateUser parameters: - name: user_id in: path + description: User ID (primary key) required: true schema: type: integer format: int64 - description: User ID (primary key) 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 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar example: 42 + nullable: true mail: + description: User email (must be unique and valid) 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 + pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' nickname: - type: string - pattern: '^[a-zA-Z0-9_-]{3,16}$' description: 'Username (alphanumeric + `_` or `-`, 3–16 chars)' + type: string + example: john_doe_43 maxLength: 16 minLength: 3 - example: john_doe_43 + pattern: '^[a-zA-Z0-9_-]{3,16}$' disp_name: - type: string description: Display name - maxLength: 32 - example: John Smith - user_desc: type: string + example: John Smith + maxLength: 32 + user_desc: description: User description / bio - maxLength: 512 + type: string example: Just a curious developer. + maxLength: 512 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). @@ -222,64 +222,64 @@ paths: parameters: - $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/title_sort' - - in: path - name: user_id + - name: user_id + in: path required: true schema: type: string - - in: query - name: sort_forward + - name: sort_forward + in: query schema: type: boolean default: true - - in: query - name: word + - name: word + in: query schema: type: string - - in: query - name: status + - name: status + in: query + description: List of title statuses to filter schema: type: array items: $ref: '#/components/schemas/TitleStatus' - description: List of title statuses to filter - style: form explode: false - - in: query - name: watch_status + style: form + - name: watch_status + in: query schema: type: array items: $ref: '#/components/schemas/UserTitleStatus' - style: form explode: false - - in: query - name: rating + style: form + - name: rating + in: query schema: type: number format: double - - in: query - name: my_rate + - name: my_rate + in: query schema: type: integer format: int32 - - in: query - name: release_year + - name: release_year + in: query schema: type: integer format: int32 - - in: query - name: release_season + - name: release_season + in: query schema: $ref: '#/components/schemas/ReleaseSeason' - - in: query - name: limit + - name: limit + in: query schema: type: integer format: int32 default: 10 - - in: query - name: fields + - name: fields + in: query schema: type: string default: all @@ -309,17 +309,17 @@ paths: '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' - operationId: addUserTitle parameters: - name: user_id in: path + description: ID of the user to assign the title to required: true schema: type: integer format: int64 - description: ID of the user to assign the title to example: 123 requestBody: required: true @@ -327,9 +327,6 @@ paths: application/json: schema: type: object - required: - - title_id - - status properties: title_id: type: integer @@ -339,36 +336,16 @@ paths: rate: type: integer format: int32 + required: + - title_id + - status responses: '200': description: Title successfully added to user content: application/json: schema: - type: object - required: - - user_id - - title_id - - status - properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time - additionalProperties: false + $ref: '#/components/schemas/UserTitleMini' '400': description: 'Invalid request body (missing fields, invalid types, etc.)' '401': @@ -382,17 +359,17 @@ paths: '500': description: Internal server error patch: + operationId: updateUserTitle summary: Update a usertitle description: User updating title list of watched - operationId: updateUserTitle parameters: - name: user_id in: path + description: ID of the user to assign the title to required: true schema: type: integer format: int64 - description: ID of the user to assign the title to example: 123 requestBody: required: true @@ -400,8 +377,6 @@ paths: application/json: schema: type: object - required: - - title_id properties: title_id: type: integer @@ -411,13 +386,15 @@ paths: rate: type: integer format: int32 + required: + - title_id responses: '200': description: Title successfully updated content: application/json: schema: - $ref: '#/paths/~1users~1%7Buser_id%7D~1titles/post/responses/200/content/application~1json/schema' + $ref: '#/components/schemas/UserTitleMini' '400': description: 'Invalid request body (missing fields, invalid types, etc.)' '401': @@ -428,6 +405,30 @@ paths: 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 + description: ID of the user to assign the title to + required: true + schema: + type: integer + format: int64 + example: 123 + 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: @@ -443,25 +444,36 @@ components: schema: $ref: '#/components/schemas/TitleSort' schemas: - CursorObj: - type: object - required: - - id - properties: - id: - type: integer - format: int64 - param: - type: string TitleSort: - type: string description: Title sort order + type: string 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: @@ -469,65 +481,11 @@ components: type: integer format: int64 storage_type: - type: string - description: Image storage type - enum: - - s3 - - local + $ref: '#/components/schemas/StorageType' 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 @@ -538,30 +496,41 @@ components: $ref: '#/components/schemas/Image' description: type: string - Title: - type: object required: - id - - title_names - - tags + - 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 properties: id: + description: Unique title ID (primary key) type: integer format: int64 - description: Unique title ID (primary key) example: 1 title_names: - type: object description: 'Localized titles. Key = language (ISO 639-1), value = list of names' - additionalProperties: - type: array - items: - type: string - example: Attack on Titan - minItems: 1 - example: - - Attack on Titan - - AoT + type: object example: en: - Attack on Titan @@ -571,6 +540,15 @@ components: - Титаны ja: - 進撃の巨人 + additionalProperties: + type: array + items: + type: string + example: Attack on Titan + minItems: 1 + example: + - Attack on Titan + - AoT studio: $ref: '#/components/schemas/Studio' tags: @@ -602,50 +580,68 @@ components: type: number format: double additionalProperties: true - User: + required: + - id + - title_names + - tags + CursorObj: type: object properties: id: type: integer format: int64 + param: + type: string + required: + - id + User: + type: object + properties: + id: description: Unique user ID (primary key) + type: integer + format: int64 example: 1 image: $ref: '#/components/schemas/Image' mail: + description: User email type: string format: email - description: User email example: john.doe@example.com nickname: - type: string description: Username (alphanumeric + _ or -) - maxLength: 16 + type: string example: john_doe_42 + maxLength: 16 disp_name: - type: string description: Display name - maxLength: 32 - example: John Doe - user_desc: type: string + example: John Doe + maxLength: 32 + user_desc: description: User description - maxLength: 512 + type: string example: Just a regular user. + maxLength: 512 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 - required: - - user_id - - title_id - - status properties: user_id: type: integer @@ -663,3 +659,34 @@ components: ctime: type: string format: date-time + required: + - user_id + - title_id + - status + UserTitleMini: + type: object + properties: + user_id: + type: integer + format: int64 + title_id: + type: integer + format: int64 + status: + $ref: '#/components/schemas/UserTitleStatus' + rate: + type: integer + format: int32 + review_id: + type: integer + format: int64 + ctime: + type: string + format: date-time + required: + - user_id + - title_id + - status + Review: + type: object + additionalProperties: true diff --git a/api/api.gen.go b/api/api.gen.go index cb5c1ae..6af01d0 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -16,12 +16,6 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) -// Defines values for ImageStorageType. -const ( - Local ImageStorageType = "local" - S3 ImageStorageType = "s3" -) - // Defines values for ReleaseSeason. const ( Fall ReleaseSeason = "fall" @@ -30,6 +24,12 @@ const ( Winter ReleaseSeason = "winter" ) +// Defines values for StorageType. +const ( + Local StorageType = "local" + S3 StorageType = "s3" +) + // Defines values for TitleSort. const ( Id TitleSort = "id" @@ -65,15 +65,15 @@ type Image struct { ImagePath *string `json:"image_path,omitempty"` // StorageType Image storage type - StorageType *ImageStorageType `json:"storage_type,omitempty"` + StorageType *StorageType `json:"storage_type,omitempty"` } -// ImageStorageType Image storage type -type ImageStorageType string - // 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"` @@ -156,6 +156,18 @@ type UserTitle struct { 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"` +} + // UserTitleStatus User's title status type UserTitleStatus string @@ -225,21 +237,30 @@ type GetUsersUserIdTitlesParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } +// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle. +type UpdateUserTitleJSONBody struct { + Rate *int32 `json:"rate,omitempty"` + + // Status User's title status + Status *UserTitleStatus `json:"status,omitempty"` + TitleId int64 `json:"title_id"` +} + // AddUserTitleJSONBody defines parameters for AddUserTitle. type AddUserTitleJSONBody struct { - Ctime *time.Time `json:"ctime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - ReviewId *int64 `json:"review_id,omitempty"` + Rate *int32 `json:"rate,omitempty"` // Status User's title status Status UserTitleStatus `json:"status"` TitleId int64 `json:"title_id"` - UserId int64 `json:"user_id"` } // UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType. type UpdateUserJSONRequestBody UpdateUserJSONBody +// UpdateUserTitleJSONRequestBody defines body for UpdateUserTitle for application/json ContentType. +type UpdateUserTitleJSONRequestBody UpdateUserTitleJSONBody + // AddUserTitleJSONRequestBody defines body for AddUserTitle for application/json ContentType. type AddUserTitleJSONRequestBody AddUserTitleJSONBody @@ -499,9 +520,15 @@ type ServerInterface interface { // Partially update a user account // (PATCH /users/{user_id}) UpdateUser(c *gin.Context, userId int64) + // Delete a usertitle + // (DELETE /users/{user_id}/titles) + DeleteUserTitle(c *gin.Context, userId int64) // Get user titles // (GET /users/{user_id}/titles) GetUsersUserIdTitles(c *gin.Context, userId string, params GetUsersUserIdTitlesParams) + // Update a usertitle + // (PATCH /users/{user_id}/titles) + UpdateUserTitle(c *gin.Context, userId int64) // Add a title to a user // (POST /users/{user_id}/titles) AddUserTitle(c *gin.Context, userId int64) @@ -716,6 +743,30 @@ func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) { siw.Handler.UpdateUser(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 + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.DeleteUserTitle(c, userId) +} + // GetUsersUserIdTitles operation middleware func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { @@ -839,6 +890,30 @@ func (siw *ServerInterfaceWrapper) GetUsersUserIdTitles(c *gin.Context) { siw.Handler.GetUsersUserIdTitles(c, userId, params) } +// 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 + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.UpdateUserTitle(c, userId) +} + // AddUserTitle operation middleware func (siw *ServerInterfaceWrapper) AddUserTitle(c *gin.Context) { @@ -894,7 +969,9 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitlesTitleId) router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId) router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser) + router.DELETE(options.BaseURL+"/users/:user_id/titles", wrapper.DeleteUserTitle) router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUsersUserIdTitles) + router.PATCH(options.BaseURL+"/users/:user_id/titles", wrapper.UpdateUserTitle) router.POST(options.BaseURL+"/users/:user_id/titles", wrapper.AddUserTitle) } @@ -1110,6 +1187,54 @@ func (response UpdateUser500Response) VisitUpdateUserResponse(w http.ResponseWri return nil } +type DeleteUserTitleRequestObject struct { + UserId int64 `json:"user_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 GetUsersUserIdTitlesRequestObject struct { UserId string `json:"user_id"` Params GetUsersUserIdTitlesParams @@ -1163,6 +1288,64 @@ func (response GetUsersUserIdTitles500Response) VisitGetUsersUserIdTitlesRespons return nil } +type UpdateUserTitleRequestObject struct { + UserId int64 `json:"user_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 { + w.WriteHeader(500) + return nil +} + type AddUserTitleRequestObject struct { UserId int64 `json:"user_id"` Body *AddUserTitleJSONRequestBody @@ -1172,16 +1355,7 @@ type AddUserTitleResponseObject interface { VisitAddUserTitleResponse(w http.ResponseWriter) error } -type AddUserTitle200JSONResponse 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"` -} +type AddUserTitle200JSONResponse UserTitleMini func (response AddUserTitle200JSONResponse) VisitAddUserTitleResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") @@ -1252,9 +1426,15 @@ type StrictServerInterface interface { // Partially update a user account // (PATCH /users/{user_id}) UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) + // Delete a usertitle + // (DELETE /users/{user_id}/titles) + DeleteUserTitle(ctx context.Context, request DeleteUserTitleRequestObject) (DeleteUserTitleResponseObject, error) // Get user titles // (GET /users/{user_id}/titles) GetUsersUserIdTitles(ctx context.Context, request GetUsersUserIdTitlesRequestObject) (GetUsersUserIdTitlesResponseObject, error) + // Update a usertitle + // (PATCH /users/{user_id}/titles) + UpdateUserTitle(ctx context.Context, request UpdateUserTitleRequestObject) (UpdateUserTitleResponseObject, error) // Add a title to a user // (POST /users/{user_id}/titles) AddUserTitle(ctx context.Context, request AddUserTitleRequestObject) (AddUserTitleResponseObject, error) @@ -1390,6 +1570,33 @@ func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64) { } } +// DeleteUserTitle operation middleware +func (sh *strictHandler) DeleteUserTitle(ctx *gin.Context, userId int64) { + var request DeleteUserTitleRequestObject + + request.UserId = userId + + 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)) + } +} + // GetUsersUserIdTitles operation middleware func (sh *strictHandler) GetUsersUserIdTitles(ctx *gin.Context, userId string, params GetUsersUserIdTitlesParams) { var request GetUsersUserIdTitlesRequestObject @@ -1418,6 +1625,41 @@ func (sh *strictHandler) GetUsersUserIdTitles(ctx *gin.Context, userId string, p } } +// UpdateUserTitle operation middleware +func (sh *strictHandler) UpdateUserTitle(ctx *gin.Context, userId int64) { + var request UpdateUserTitleRequestObject + + request.UserId = userId + + 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 { + 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 diff --git a/api/openapi.yaml b/api/openapi.yaml index 7da26f8..23f2058 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -21,4 +21,3 @@ components: $ref: "./parameters/_index.yaml" schemas: $ref: "./schemas/_index.yaml" - \ No newline at end of file diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index 1580cc1..18c805e 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -117,11 +117,10 @@ post: type: integer format: int64 status: - $ref: ../schemas/enums/UserTitleStatus.yaml + $ref: '../schemas/enums/UserTitleStatus.yaml' rate: type: integer format: int32 - responses: '200': description: Title successfully added to user @@ -169,7 +168,7 @@ patch: type: integer format: int64 status: - $ref: ../schemas/enums/UserTitleStatus.yaml + $ref: '../schemas/enums/UserTitleStatus.yaml' rate: type: integer format: int32 @@ -190,5 +189,33 @@ patch: 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: + - name: user_id + in: path + required: true + schema: + type: integer + format: int64 + description: ID of the user to assign the title to + example: 123 + + responses: + '200': + description: Title successfully deleted + # '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 delete title + '404': + description: User or Title not found '500': description: Internal server error \ No newline at end of file diff --git a/api/schemas/UserTitleMini.yaml b/api/schemas/UserTitleMini.yaml index 9e45e95..e20bcbf 100644 --- a/api/schemas/UserTitleMini.yaml +++ b/api/schemas/UserTitleMini.yaml @@ -20,5 +20,4 @@ properties: format: int64 ctime: type: string - format: date-time -additionalProperties: false + format: date-time \ No newline at end of file diff --git a/deploy/api_gen.ps1 b/deploy/api_gen.ps1 new file mode 100644 index 0000000..c8966b7 --- /dev/null +++ b/deploy/api_gen.ps1 @@ -0,0 +1,4 @@ +cd ./api +openapi-format .\openapi.yaml --output .\_build\openapi.yaml --yaml +cd .. +oapi-codegen --config=api\oapi-codegen.yaml api\_build\openapi.yaml From f2589e05e8ebef3bfbe6342310757dca3dafe983 Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Thu, 27 Nov 2025 07:06:18 +0300 Subject: [PATCH 2/3] fix: now 409 on try to add existing usertitle --- modules/backend/handlers/common.go | 7 ++--- modules/backend/handlers/titles.go | 4 +-- modules/backend/handlers/users.go | 50 +++++++++++++++++------------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go index 2cf2283..f820db6 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -1,7 +1,6 @@ package handlers import ( - "context" "encoding/json" "fmt" oapi "nyanimedb/api" @@ -17,11 +16,11 @@ func NewServer(db *sqlc.Queries) Server { return Server{db: db} } -func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.ImageStorageType, error) { +func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.StorageType, error) { if s == nil { return nil, nil } - var t oapi.ImageStorageType + var t oapi.StorageType switch *s { case sqlc.StorageTypeTLocal: t = oapi.Local @@ -33,7 +32,7 @@ func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.ImageStorageType, error) { return &t, nil } -func (s Server) mapTitle(ctx context.Context, title sqlc.GetTitleByIDRow) (oapi.Title, error) { +func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) { oapi_title := oapi.Title{ EpisodesAired: title.EpisodesAired, diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go index c67177f..03553fd 100644 --- a/modules/backend/handlers/titles.go +++ b/modules/backend/handlers/titles.go @@ -144,7 +144,7 @@ func (s Server) GetTitlesTitleId(ctx context.Context, request oapi.GetTitlesTitl return oapi.GetTitlesTitleId500Response{}, nil } - oapi_title, err = s.mapTitle(ctx, sqlc_title) + oapi_title, err = s.mapTitle(sqlc_title) if err != nil { log.Errorf("%v", err) return oapi.GetTitlesTitleId500Response{}, nil @@ -238,7 +238,7 @@ func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObje // _title.TitleStorageType = string(s) // } - t, err := s.mapTitle(ctx, _title) + t, err := s.mapTitle(_title) if err != nil { log.Errorf("%v", err) return oapi.GetTitles500Response{}, nil diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index 1881f36..7af705e 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "errors" "fmt" oapi "nyanimedb/api" sqlc "nyanimedb/sql" @@ -9,24 +10,12 @@ import ( "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" ) -// type Server struct { -// db *sqlc.Queries -// } - -// func NewServer(db *sqlc.Queries) Server { -// return Server{db: db} -// } - -// func parseInt64(s string) (int32, error) { -// i, err := strconv.ParseInt(s, 10, 64) -// return int32(i), err -// } - func mapUser(u sqlc.GetUserByIDRow) (oapi.User, error) { i := oapi.Image{ Id: u.AvatarID, @@ -202,7 +191,7 @@ func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (o // StudioImagePath: title.StudioImagePath, } - oapi_title, err := s.mapTitle(ctx, _title) + oapi_title, err := s.mapTitle(_title) if err != nil { return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) } @@ -368,19 +357,26 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque } params := sqlc.InsertUserTitleParams{ - UserID: request.UserId, - TitleID: request.Body.TitleId, - Status: *status, - Rate: request.Body.Rate, - ReviewID: request.Body.ReviewId, + UserID: request.UserId, + TitleID: request.Body.TitleId, + Status: *status, + Rate: request.Body.Rate, } user_title, err := s.db.InsertUserTitle(ctx, params) if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, 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 == "23505" { //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) @@ -406,3 +402,13 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque return oapi.AddUserTitle200JSONResponse(oapi_usertitle), nil } + +// DeleteUserTitle implements oapi.StrictServerInterface. +func (s Server) DeleteUserTitle(ctx context.Context, request oapi.DeleteUserTitleRequestObject) (oapi.DeleteUserTitleResponseObject, error) { + panic("unimplemented") +} + +// UpdateUserTitle implements oapi.StrictServerInterface. +func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitleRequestObject) (oapi.UpdateUserTitleResponseObject, error) { + panic("unimplemented") +} From 658d666fec71c165de4492111bfa9d29f42cef18 Mon Sep 17 00:00:00 2001 From: Iron_Felix Date: Thu, 27 Nov 2025 07:08:06 +0300 Subject: [PATCH 3/3] feat: query for update usertitle --- modules/backend/queries.sql | 28 ++++++++--------------- sql/migrations/000001_init.up.sql | 2 +- sql/queries.sql.go | 38 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 0146b25..ef6e26d 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -461,21 +461,13 @@ VALUES ( ) RETURNING user_id, title_id, status, rate, review_id, ctime; --- -- 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 +-- 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 *; \ No newline at end of file diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index f8781de..3499fe2 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -179,6 +179,6 @@ END; $$ LANGUAGE plpgsql; CREATE TRIGGER set_ctime_on_update -AFTER UPDATE ON usertitles +BEFORE UPDATE ON usertitles FOR EACH ROW EXECUTE FUNCTION set_ctime(); \ No newline at end of file diff --git a/sql/queries.sql.go b/sql/queries.sql.go index a46da86..89b16c9 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -925,3 +925,41 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateU ) 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 NullUsertitleStatusT `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 +}