diff --git a/internal/merch/controller.go b/internal/merch/controller.go index 24aed98..eae6d52 100644 --- a/internal/merch/controller.go +++ b/internal/merch/controller.go @@ -1,6 +1,7 @@ package merch import ( + "errors" "github.com/gin-gonic/gin" "github.com/google/uuid" "merch-api/pkg/responses" @@ -26,7 +27,8 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { merchGroup.POST("/create", h.controller.create) merchGroup.GET("/:id", h.controller.getOne) merchGroup.GET("/list", h.controller.getMany) - merchGroup.PUT("/update", h.controller.update) + merchGroup.PUT("/:id", h.controller.updateMerch) + merchGroup.PUT("/extra/:id", h.controller.updateExtraData) merchGroup.DELETE("/:id", h.controller.deleteMerch) originsGroup := merchGroup.Group("/origins") @@ -49,21 +51,14 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { // @Failure 500 {object} responses.InternalServerError // @Router /merch/create [POST] func (co *controller) create(c *gin.Context) { - userUuid, err := co.utils.GetUserUuidFromContext(c) - if err != nil { - c.JSON(http.StatusUnauthorized, responses.Unauthorized{Error: err.Error()}) - logErrController(err) - return - } - var newMerch newMerchDTO - if err = c.ShouldBindJSON(&newMerch); err != nil { + if err := c.ShouldBindJSON(&newMerch); err != nil { c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) logErrController(err) return } - if err = co.service.createMerch(c, userUuid, &newMerch); err != nil { + if err := co.service.createMerch(c, getUserId(c), &newMerch); err != nil { c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) logErrController(err) return @@ -87,14 +82,7 @@ func (co *controller) getOne(c *gin.Context) {} // @Failure 500 {object} responses.InternalServerError // @Router /merch/list [GET] func (co *controller) getMany(c *gin.Context) { - userUuid, err := co.utils.GetUserUuidFromContext(c) - if err != nil { - c.JSON(http.StatusUnauthorized, responses.Unauthorized{Error: err.Error()}) - logErrController(err) - return - } - - response, err := co.service.getMany(c, userUuid) + response, err := co.service.getMany(c, getUserId(c)) if err != nil { c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) logErrController(err) @@ -109,7 +97,95 @@ func (co *controller) getMany(c *gin.Context) { c.JSON(http.StatusOK, response) } -func (co *controller) update(c *gin.Context) {} +// updateMerch godoc +// +// @Summary Update merch +// @Description Update merch general info (except extra data) +// @Tags Merch +// @Accept json +// @Param uuid path string true "merch uuid" +// @Param payload body updateMerchDTO true "payload" +// @Produce json +// @Success 200 {object} merchDTO +// @Failure 400 {object} responses.BadRequest +// @Failure 401 {object} responses.Unauthorized +// @Failure 500 {object} responses.InternalServerError +// @Router /merch/{uuid} [PUT] +func (co *controller) updateMerch(c *gin.Context) { + merchUuid := c.Param("id") + if err := uuid.Validate(merchUuid); err != nil { + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) + logErrController(err) + return + } + + var payload updateMerchDTO + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) + logErrController(err) + return + } + + if merchUuid != payload.MerchUuid { + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: "MerchUuid does not match"}) + logErrController(errors.New("MerchUuid does not match")) + return + } + + response, err := co.service.updateMerch(c, getUserId(c), &payload) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) + logErrController(err) + return + } + + c.JSON(http.StatusOK, response) +} + +// updateMerch godoc +// +// @Summary Update merch extra data +// @Description Update ONLY merch extra data +// @Tags Merch +// @Accept json +// @Param uuid path string true "merch uuid" +// @Param payload body extraDataDTO true "payload" +// @Produce json +// @Success 200 {object} extraDataDTO +// @Failure 400 {object} responses.BadRequest +// @Failure 401 {object} responses.Unauthorized +// @Failure 500 {object} responses.InternalServerError +// @Router /merch/extra/{uuid} [PUT] +func (co *controller) updateExtraData(c *gin.Context) { + merchUuid := c.Param("id") + if err := uuid.Validate(merchUuid); err != nil { + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) + logErrController(err) + return + } + + var payload extraDataDTO + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) + logErrController(err) + return + } + + if merchUuid != payload.MerchUuid { + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: "MerchUuid does not match"}) + logErrController(errors.New("MerchUuid does not match")) + return + } + + response, err := co.service.updateExtraData(c, getUserId(c), &payload) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) + logErrController(err) + return + } + + c.JSON(http.StatusOK, response) +} // deleteMerch godoc // @@ -124,21 +200,14 @@ func (co *controller) update(c *gin.Context) {} // @Failure 500 {object} responses.InternalServerError // @Router /merch/{uuid} [DELETE] func (co *controller) deleteMerch(c *gin.Context) { - userUuid, err := co.utils.GetUserUuidFromContext(c) - if err != nil { - c.JSON(http.StatusUnauthorized, responses.Unauthorized{Error: err.Error()}) - logErrController(err) - return - } - merchUuid := c.Param("id") - if err = uuid.Validate(merchUuid); err != nil { + if err := uuid.Validate(merchUuid); err != nil { c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) logErrController(err) return } - if err = co.service.deleteOneMerchRecord(c, userUuid, merchUuid); err != nil { + if err := co.service.deleteOneMerchRecord(c, getUserId(c), merchUuid); err != nil { c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) logErrController(err) return diff --git a/internal/merch/dto.go b/internal/merch/dto.go index ccd0b29..dbc747f 100644 --- a/internal/merch/dto.go +++ b/internal/merch/dto.go @@ -25,8 +25,8 @@ type newMerchDTO struct { } type originLink struct { - Name string `json:"origin_name"` - Link string `json:"origin_link"` + Origin string `json:"origin"` + Link string `json:"origin_link"` } type merchDTO struct { @@ -35,3 +35,13 @@ type merchDTO struct { MerchUuid string `json:"merch_uuid"` Name string `json:"name"` } + +type updateMerchDTO struct { + MerchUuid string `json:"merch_uuid"` + Name string `json:"name"` +} + +type extraDataDTO struct { + MerchUuid string `json:"merch_uuid"` + Links []originLink `json:"links"` +} diff --git a/internal/merch/model.go b/internal/merch/model.go index 7a2d734..e5f7dde 100644 --- a/internal/merch/model.go +++ b/internal/merch/model.go @@ -11,7 +11,7 @@ type Merch struct { UpdatedAt sql.NullTime DeletedAt sql.NullTime MerchUuid string - UserId string + UserId int64 Name string } diff --git a/internal/merch/repository.go b/internal/merch/repository.go index 5f5e0c4..56e7dc6 100644 --- a/internal/merch/repository.go +++ b/internal/merch/repository.go @@ -3,6 +3,7 @@ package merch import ( "context" "database/sql" + "errors" "fmt" "github.com/jackc/pgx/v5/pgxpool" "strings" @@ -14,10 +15,15 @@ type Repository interface { createMerch(ctx context.Context, merch *Merch, extra []ExtraData) error // getMany returns list of only main merch record, without origins extra data - getMany(ctx context.Context, userId string) ([]merchDTO, error) + getMany(ctx context.Context, userId int64) ([]merchDTO, error) + + getMerchIdByUuid(ctx context.Context, userId int64, uuid string) (int64, error) + + updateMerch(ctx context.Context, userId int64, merch *updateMerchDTO) (*merchDTO, error) + updateExtraData(ctx context.Context, merchId int64, insertData []ExtraData) ([]ExtraData, error) // deleteOneMerchRecord sets deleted_at in merch + extra tables - deleteOneMerchRecord(ctx context.Context, userId, merchUuid string, delTime time.Time) error + deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string, delTime time.Time) error Origins } @@ -135,7 +141,7 @@ func (r *repo) createMerch(ctx context.Context, merch *Merch, extra []ExtraData) return tx.Commit(ctx) } -func (r *repo) getMany(ctx context.Context, userId string) ([]merchDTO, error) { +func (r *repo) getMany(ctx context.Context, userId int64) ([]merchDTO, error) { q := `SELECT created_at, updated_at, merch_uuid, name FROM merch WHERE deleted_at IS NULL AND user_id = $1` rows, err := r.db.Query(ctx, q, userId) @@ -158,7 +164,22 @@ func (r *repo) getMany(ctx context.Context, userId string) ([]merchDTO, error) { return result, nil } -func (r *repo) deleteOneMerchRecord(ctx context.Context, userId, merchUuid string, delTime time.Time) error { +func (r *repo) getMerchIdByUuid(ctx context.Context, userId int64, uuid string) (int64, error) { + q := `SELECT id FROM merch WHERE deleted_at IS NULL AND merch_uuid = $1 AND user_id = $2` + + var merchId int64 + if err := r.db.QueryRow(ctx, q, uuid, userId).Scan(&merchId); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return 0, nil + } else { + return 0, err + } + } + + return merchId, nil +} + +func (r *repo) deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string, delTime time.Time) error { tx, err := r.db.Begin(ctx) if err != nil { return err @@ -178,3 +199,79 @@ func (r *repo) deleteOneMerchRecord(ctx context.Context, userId, merchUuid strin return tx.Commit(ctx) } + +func (r *repo) updateMerch(ctx context.Context, userId int64, merch *updateMerchDTO) (*merchDTO, error) { + q := `UPDATE merch SET name = $1 WHERE merch_uuid = $2 AND user_id = $3 + RETURNING created_at, updated_at, merch_uuid, name` + + var result merchDTO + if err := r.db. + QueryRow(ctx, q, merch.Name, merch.MerchUuid, userId). + Scan(&result.CreatedAt, &result.UpdatedAt, &result.MerchUuid, &result.Name); err != nil { + return nil, err + } + + return &result, nil +} + +//q := ` +// UPDATE merch_extra_data AS med +// SET url = COALESCE(NULLIF(src.new_url, ''), med.url) +// FROM UNNEST( +// $1::text[], +// $2::bigint[] +// ) AS src(new_url, origin_id) +// WHERE med.merch_id = $3 +// AND med.origin_id = src.origin_id +// RETURNING med.created_at, med.updated_at, med.merch_id, med.origin_id, med.url +// ` + +func (r *repo) updateExtraData(ctx context.Context, merchId int64, insertData []ExtraData) ([]ExtraData, error) { + q := ` + INSERT INTO merch_extra_data (merch_id, origin_id, url, created_at, updated_at) + SELECT $1, src.origin_id, src.new_url, src.created_at, src.updated_at + FROM UNNEST( + $2::text[], + $3::bigint[], + $4::timestamptz[], + $5::timestamptz[]) + AS src(new_url, origin_id, created_at, updated_at) + ON CONFLICT (merch_id, origin_id) DO UPDATE SET url = COALESCE(NULLIF(EXCLUDED.url, ''), merch_extra_data.url), updated_at = NOW() + RETURNING created_at, updated_at, merch_id, origin_id, url; + ` + + var ( + urls []string + origins []int64 + createdAt []time.Time + updatedAt []sql.NullTime + ) + + for _, data := range insertData { + urls = append(urls, data.URL) + origins = append(origins, data.OriginId) + createdAt = append(createdAt, data.CreatedAt) + updatedAt = append(updatedAt, data.UpdatedAt) + } + + rows, err := r.db.Query(ctx, q, merchId, urls, origins, createdAt, updatedAt) + if err != nil { + return nil, err + } + + var result []ExtraData + for rows.Next() { + var m ExtraData + if err = rows.Scan(&m.CreatedAt, &m.UpdatedAt, &m.MerchId, &m.OriginId, &m.URL); err != nil { + return nil, err + } + result = append(result, m) + } + + rows.Close() //must be before rows.Err check! + if err = rows.Err(); err != nil { + return nil, err + } + + return result, nil +} diff --git a/internal/merch/service.go b/internal/merch/service.go index d8e8c3d..1b1a1f0 100644 --- a/internal/merch/service.go +++ b/internal/merch/service.go @@ -65,13 +65,7 @@ func (s *service) deleteOrigin(ctx context.Context, origin *deleteOriginDTO) err // merch -func (s *service) createMerch(ctx context.Context, userUuid string, payload *newMerchDTO) error { - userId, err := s.userProvider.GetUserId(ctx, userUuid) - if err != nil { - logErrService(err) - return err - } - +func (s *service) createMerch(ctx context.Context, userId int64, payload *newMerchDTO) error { now := s.utils.TimeNowUTC() nullNow := s.utils.NullTimeFromNow(now) empty := s.utils.DeletedNullTime() @@ -94,7 +88,7 @@ func (s *service) createMerch(ctx context.Context, userUuid string, payload *new var merchExtra []ExtraData if payload.Links != nil { - originsMap, err := s.getOriginsMap(ctx) + originsMap, _, err := s.getOriginsMaps(ctx) if err != nil { logErrService(err) return err @@ -106,7 +100,7 @@ func (s *service) createMerch(ctx context.Context, userUuid string, payload *new UpdatedAt: nullNow, DeletedAt: empty, MerchId: 0, - OriginId: originsMap[item.Name], + OriginId: originsMap[item.Origin], URL: item.Link, }) } @@ -115,37 +109,85 @@ func (s *service) createMerch(ctx context.Context, userUuid string, payload *new return s.repo.createMerch(ctx, newMerch, merchExtra) } -func (s *service) getOriginsMap(ctx context.Context) (map[string]int64, error) { +func (s *service) getOriginsMaps(ctx context.Context) (map[string]int64, map[int64]string, error) { origins, err := s.repo.getOrigins(ctx) if err != nil { logErrService(err) - return nil, err + return nil, nil, err } - originsMap := make(map[string]int64, len(origins)) + namesMap := make(map[string]int64, len(origins)) + idsMap := make(map[int64]string, len(origins)) + for _, origin := range origins { - originsMap[origin.Name] = origin.Id + namesMap[origin.Name] = origin.Id + idsMap[origin.Id] = origin.Name } - return originsMap, nil + return namesMap, idsMap, nil } -func (s *service) getMany(ctx context.Context, userUuid string) ([]merchDTO, error) { - userId, err := s.userProvider.GetUserId(ctx, userUuid) - if err != nil { - logErrService(err) - return nil, err - } - +func (s *service) getMany(ctx context.Context, userId int64) ([]merchDTO, error) { return s.repo.getMany(ctx, userId) } -func (s *service) deleteOneMerchRecord(ctx context.Context, userUuid, merchUuid string) error { - userId, err := s.userProvider.GetUserId(ctx, userUuid) +func (s *service) updateMerch(ctx context.Context, userId int64, payload *updateMerchDTO) (*merchDTO, error) { + return s.repo.updateMerch(ctx, userId, payload) +} + +func (s *service) updateExtraData(ctx context.Context, userId int64, payload *extraDataDTO) (*extraDataDTO, error) { + merchId, err := s.repo.getMerchIdByUuid(ctx, userId, payload.MerchUuid) if err != nil { logErrService(err) - return err + return nil, err } + origins, ids, err := s.getOriginsMaps(ctx) + if err != nil { + logErrService(err) + return nil, err + } + + var insertData []ExtraData + for _, item := range payload.Links { + if _, ok := origins[item.Origin]; !ok { + continue + } + + now := s.utils.TimeNowUTC() + + insertData = append(insertData, ExtraData{ + CreatedAt: now, + UpdatedAt: s.utils.NullTimeFromNow(now), + OriginId: origins[item.Origin], + URL: item.Link, + }) + } + + result, err := s.repo.updateExtraData(ctx, merchId, insertData) + if err != nil { + logErrService(err) + return nil, err + } + + if result == nil || len(result) == 0 { + return nil, nil + } + + var ol []originLink + for _, item := range result { + ol = append(ol, originLink{ + Origin: ids[item.OriginId], + Link: item.URL, + }) + } + + return &extraDataDTO{ + MerchUuid: payload.MerchUuid, + Links: ol, + }, nil +} + +func (s *service) deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string) error { return s.repo.deleteOneMerchRecord(ctx, userId, merchUuid, s.utils.TimeNowUTC()) }