From 97f8d27430fa556cddc0c8e6a6ead2f62a9d47f4 Mon Sep 17 00:00:00 2001 From: nquidox Date: Wed, 4 Mar 2026 17:02:11 +0300 Subject: [PATCH] switch to pgx driver + create merch --- cmd/main.go | 9 ++-- internal/app/handler.go | 25 +++++++--- internal/merch/controller.go | 47 +++++++++++++++-- internal/merch/dto.go | 14 +++++- internal/merch/handler.go | 12 +++-- internal/merch/model.go | 19 +++++-- internal/merch/repository.go | 82 +++++++++++++++++++++++++----- internal/merch/service.go | 97 ++++++++++++++++++++++++++++++++---- internal/user/handler.go | 4 +- internal/user/interface.go | 4 +- internal/user/repository.go | 15 +++--- internal/user/service.go | 5 +- pkg/dbase/handler.go | 25 ++++++---- 13 files changed, 289 insertions(+), 69 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 7e60809..6c15f0c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,18 +2,19 @@ package main import ( "context" - log "github.com/sirupsen/logrus" "merch-api/config" _ "merch-api/docs" "merch-api/internal/app" "os" "os/signal" "syscall" + + log "github.com/sirupsen/logrus" ) // @Title Merch API -// @Version 2.3 -// @Description Stores data about merch and prices +// @Version 2.3 +// @Description Stores data about merch and prices // @BasePath /api/v2 func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) @@ -22,7 +23,7 @@ func main() { cfg := config.NewConfig() config.LogSetup(cfg.App.Mode, cfg.App.LogLvl) - appl := app.New(cfg) + appl := app.New(ctx, cfg) if err := appl.Run(ctx); err != nil { log.Fatal(err) diff --git a/internal/app/handler.go b/internal/app/handler.go index ed1dcd6..3121362 100644 --- a/internal/app/handler.go +++ b/internal/app/handler.go @@ -2,14 +2,17 @@ package app import ( "context" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" + "github.com/jackc/pgx/v5/pgxpool" "merch-api/config" "merch-api/internal/merch" + "merch-api/internal/user" "merch-api/pkg/dbase" "merch-api/pkg/router" "merch-api/pkg/utils" "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" ) const pkgLogHeader string = "Application |" @@ -18,9 +21,10 @@ type App struct { cfg config.Config router *router.Router modules []Module + dbPool *pgxpool.Pool } -func New(cfg config.Config) *App { +func New(ctx context.Context, cfg config.Config) *App { //providers r := router.NewRouter(router.Deps{ Host: cfg.Http.Host, @@ -29,7 +33,7 @@ func New(cfg config.Config) *App { GinMode: cfg.Http.GinMode, }) - db, err := dbase.Connect(dbase.Deps{ + dbPool, err := dbase.ConnectPool(ctx, dbase.Deps{ Host: cfg.DBase.Host, Port: cfg.DBase.Port, Username: cfg.DBase.Username, @@ -39,6 +43,11 @@ func New(cfg config.Config) *App { u := utils.New() + userProv := user.New(user.Deps{ + DB: dbPool, + Utils: u, + }) + if err != nil { log.WithError(err).Fatalf("%v failed to connect database", pkgLogHeader) } @@ -47,8 +56,9 @@ func New(cfg config.Config) *App { var modules []Module m := merch.New(merch.Deps{ - DB: db, - Utils: u, + DB: dbPool, + Utils: u, + UserProvider: userProv, }) modules = append(modules, m) @@ -56,6 +66,7 @@ func New(cfg config.Config) *App { cfg: cfg, router: r, modules: modules, + dbPool: dbPool, } } @@ -88,6 +99,8 @@ func (app *App) shutdown(ctx context.Context) { shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 15*time.Second) defer shutdownCancel() + app.dbPool.Close() + if err := app.router.Shutdown(shutdownCtx); err != nil { log.WithError(err).Warnf("%v error shutting down application", pkgLogHeader) } diff --git a/internal/merch/controller.go b/internal/merch/controller.go index 41961a5..8e9a4e1 100644 --- a/internal/merch/controller.go +++ b/internal/merch/controller.go @@ -3,16 +3,19 @@ package merch import ( "github.com/gin-gonic/gin" "merch-api/pkg/responses" + "merch-api/pkg/utils" "net/http" ) type controller struct { service *service + utils utils.Utils } -func newController(s *service) *controller { +func newController(s *service, u utils.Utils) *controller { return &controller{ service: s, + utils: u, } } @@ -32,7 +35,41 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { } -func (co *controller) create(c *gin.Context) {} +// create godoc +// +// @Summary Create new merch +// @Description Create new merch +// @Tags Merch +// @Accept json +// @Param merch body newMerchDTO true "merch body" +// @Success 201 +// @Failure 400 {object} responses.BadRequest +// @Failure 401 {object} responses.Unauthorized +// @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 { + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) + logErrController(err) + return + } + + if err = co.service.createMerch(c, userUuid, &newMerch); err != nil { + c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) + logErrController(err) + return + } + + c.Status(http.StatusCreated) +} func (co *controller) getOne(c *gin.Context) {} @@ -63,7 +100,7 @@ func (co *controller) createOrigin(c *gin.Context) { return } - if err := co.service.createOrigin(origin); err != nil { + if err := co.service.createOrigin(c, origin); err != nil { c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) logErrController(err) return @@ -86,7 +123,7 @@ func (co *controller) createOrigin(c *gin.Context) { // @Failure 500 {object} responses.InternalServerError // @Router /merch/origins [GET] func (co *controller) getOrigins(c *gin.Context) { - response, err := co.service.getOrigins() + response, err := co.service.getOrigins(c) if err != nil { c.JSON(http.StatusInternalServerError, responses.InternalServerError{Error: err.Error()}) logErrController(err) @@ -118,7 +155,7 @@ func (co *controller) deleteOrigin(c *gin.Context) { return } - if err := co.service.deleteOrigin(origin); err != nil { + if err := co.service.deleteOrigin(c, origin); 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 8607a5c..f7b2304 100644 --- a/internal/merch/dto.go +++ b/internal/merch/dto.go @@ -1,5 +1,6 @@ package merch +// Origins type newOriginDTO struct { Name string `json:"name"` } @@ -7,10 +8,21 @@ type originsDTO struct { Origins []originItem `json:"origins"` } type originItem struct { - Id uint `json:"id"` + Id int64 `json:"id"` Name string `json:"name"` } type deleteOriginDTO struct { Name string `json:"name"` } + +// Merch +type newMerchDTO struct { + Name string `json:"name"` + Links []originLink `json:"links"` +} + +type originLink struct { + Name string `json:"origin_name"` + Link string `json:"origin_link"` +} diff --git a/internal/merch/handler.go b/internal/merch/handler.go index a469151..08918f1 100644 --- a/internal/merch/handler.go +++ b/internal/merch/handler.go @@ -1,7 +1,8 @@ package merch import ( - "database/sql" + "github.com/jackc/pgx/v5/pgxpool" + "merch-api/internal/user" "merch-api/pkg/utils" ) @@ -10,14 +11,15 @@ type Handler struct { } type Deps struct { - DB *sql.DB - Utils utils.Utils + DB *pgxpool.Pool + Utils utils.Utils + UserProvider user.Provider } func New(deps Deps) *Handler { r := newRepo(deps.DB) - s := newService(r, deps.Utils) - c := newController(s) + s := newService(r, deps.Utils, deps.UserProvider) + c := newController(s, deps.Utils) return &Handler{ controller: c, diff --git a/internal/merch/model.go b/internal/merch/model.go index 82d75ad..7a2d734 100644 --- a/internal/merch/model.go +++ b/internal/merch/model.go @@ -6,7 +6,7 @@ import ( ) type Merch struct { - Id uint + Id int64 CreatedAt time.Time UpdatedAt sql.NullTime DeletedAt sql.NullTime @@ -17,7 +17,7 @@ type Merch struct { // Origin model. Table name: merch_origins type Origin struct { - Id uint + Id int64 CreatedAt time.Time DeletedAt sql.NullTime Name string @@ -25,11 +25,22 @@ type Origin struct { // Price model. Table name: merch_prices type Price struct { - Id uint + Id int64 CreatedAt time.Time UpdatedAt sql.NullTime DeletedAt sql.NullTime MerchUuid string Price int - OriginId uint + OriginId int64 +} + +// ExtraData model. Table name: merch_extra_data +type ExtraData struct { + Id int64 + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime + MerchId int64 + OriginId int64 + URL string } diff --git a/internal/merch/repository.go b/internal/merch/repository.go index a5fb03f..fa36c09 100644 --- a/internal/merch/repository.go +++ b/internal/merch/repository.go @@ -1,31 +1,39 @@ package merch -import "database/sql" +import ( + "context" + "database/sql" + "fmt" + "github.com/jackc/pgx/v5/pgxpool" + "strings" +) type Repository interface { + createMerch(ctx context.Context, merch *Merch, extra []ExtraData) error + Origins } type Origins interface { - createOrigin(origin *Origin) error - getOrigins() ([]Origin, error) - deleteOriginByName(name string, deletedAt sql.NullTime) error + createOrigin(ctx context.Context, origin *Origin) error + getOrigins(ctx context.Context) ([]Origin, error) + deleteOriginByName(ctx context.Context, name string, deletedAt sql.NullTime) error } type repo struct { - db *sql.DB + db *pgxpool.Pool } -func newRepo(db *sql.DB) Repository { +func newRepo(db *pgxpool.Pool) Repository { return &repo{ db: db, } } -func (r *repo) createOrigin(origin *Origin) error { +func (r *repo) createOrigin(ctx context.Context, origin *Origin) error { q := `INSERT INTO merch_origins (created_at, deleted_at, name) VALUES ($1, $2, $3)` - _, err := r.db.Exec(q, origin.CreatedAt, origin.DeletedAt, origin.Name) + _, err := r.db.Exec(ctx, q, origin.CreatedAt, origin.DeletedAt, origin.Name) if err != nil { return err } @@ -33,10 +41,10 @@ func (r *repo) createOrigin(origin *Origin) error { return nil } -func (r *repo) getOrigins() ([]Origin, error) { +func (r *repo) getOrigins(ctx context.Context) ([]Origin, error) { q := `SELECT * FROM merch_origins WHERE deleted_at IS NULL` - rows, err := r.db.Query(q) + rows, err := r.db.Query(ctx, q) if err != nil { return nil, err } @@ -58,13 +66,63 @@ func (r *repo) getOrigins() ([]Origin, error) { return origins, nil } -func (r *repo) deleteOriginByName(name string, deletedAt sql.NullTime) error { +func (r *repo) deleteOriginByName(ctx context.Context, name string, deletedAt sql.NullTime) error { q := `UPDATE merch_origins SET deleted_at = $1 WHERE name = $2` - _, err := r.db.Exec(q, deletedAt.Time, name) + _, err := r.db.Exec(ctx, q, deletedAt.Time, name) if err != nil { return err } return nil } + +//Merch crud + +func (r *repo) createMerch(ctx context.Context, merch *Merch, extra []ExtraData) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return err + } + + qMerch := `INSERT INTO merch (created_at, updated_at, deleted_at, merch_uuid, user_id, name) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id` + + var merchId int64 + err = tx. + QueryRow(ctx, qMerch, merch.CreatedAt, merch.UpdatedAt, merch.DeletedAt, merch.MerchUuid, merch.UserId, merch.Name). + Scan(&merchId) + if err != nil { + tx.Rollback(ctx) + return err + } + + if extra == nil { + return tx.Commit(ctx) + } + + countArgs := 1 + var insertFields []string + var insertArgs []interface{} + + for _, item := range extra { + insertFields = append(insertFields, fmt.Sprintf("($%v, $%v, $%v, $%v, $%v, $%v)", + countArgs, countArgs+1, countArgs+2, countArgs+3, countArgs+4, countArgs+5)) + + insertArgs = append(insertArgs, item.CreatedAt, item.UpdatedAt, item.DeletedAt, merchId, item.OriginId, item.URL) + + countArgs += 6 + } + + qExtra := fmt.Sprintf( + "INSERT INTO merch_extra_data (created_at, updated_at, deleted_at, merch_id, origin_id, url) VALUES %v", + strings.Join(insertFields, ",")) + + _, err = tx.Exec(ctx, qExtra, insertArgs...) + if err != nil { + tx.Rollback(ctx) + return err + } + + return tx.Commit(ctx) +} diff --git a/internal/merch/service.go b/internal/merch/service.go index e55c05d..02b92b1 100644 --- a/internal/merch/service.go +++ b/internal/merch/service.go @@ -1,22 +1,30 @@ package merch import ( + "context" + "merch-api/internal/user" "merch-api/pkg/utils" + + "github.com/google/uuid" ) type service struct { - repo Repository - utils utils.Utils + repo Repository + utils utils.Utils + userProvider user.Provider } -func newService(repo Repository, u utils.Utils) *service { +func newService(repo Repository, u utils.Utils, up user.Provider) *service { return &service{ - repo: repo, - utils: u, + repo: repo, + utils: u, + userProvider: up, } } -func (s *service) createOrigin(o *newOriginDTO) error { +//origins + +func (s *service) createOrigin(ctx context.Context, o *newOriginDTO) error { newOrigin := &Origin{ CreatedAt: s.utils.TimeNowUTC(), DeletedAt: s.utils.DeletedNullTime(), @@ -24,11 +32,11 @@ func (s *service) createOrigin(o *newOriginDTO) error { } logDebugService("create origin success") - return s.repo.createOrigin(newOrigin) + return s.repo.createOrigin(ctx, newOrigin) } -func (s *service) getOrigins() (*originsDTO, error) { - data, err := s.repo.getOrigins() +func (s *service) getOrigins(ctx context.Context) (*originsDTO, error) { + data, err := s.repo.getOrigins(ctx) if err != nil { logErrService(err) return nil, err @@ -50,7 +58,74 @@ func (s *service) getOrigins() (*originsDTO, error) { return response, nil } -func (s *service) deleteOrigin(origin *deleteOriginDTO) error { +func (s *service) deleteOrigin(ctx context.Context, origin *deleteOriginDTO) error { logDebugService("delete origin success") - return s.repo.deleteOriginByName(origin.Name, s.utils.NullTimeNowUTC()) + return s.repo.deleteOriginByName(ctx, origin.Name, s.utils.NullTimeNowUTC()) +} + +// 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 + } + + now := s.utils.TimeNowUTC() + nullNow := s.utils.NullTimeFromNow(now) + empty := s.utils.DeletedNullTime() + + merchUuid, err := uuid.NewV7() + if err != nil { + logErrService(err) + return err + } + + newMerch := &Merch{ + CreatedAt: now, + UpdatedAt: nullNow, + DeletedAt: empty, + MerchUuid: merchUuid.String(), + UserId: userId, + Name: payload.Name, + } + + var merchExtra []ExtraData + if payload.Links != nil { + + originsMap, err := s.getOriginsMap(ctx) + if err != nil { + logErrService(err) + return err + } + + for _, item := range payload.Links { + merchExtra = append(merchExtra, ExtraData{ + CreatedAt: now, + UpdatedAt: nullNow, + DeletedAt: empty, + MerchId: 0, + OriginId: originsMap[item.Name], + URL: item.Link, + }) + } + } + + return s.repo.createMerch(ctx, newMerch, merchExtra) +} + +func (s *service) getOriginsMap(ctx context.Context) (map[string]int64, error) { + origins, err := s.repo.getOrigins(ctx) + if err != nil { + logErrService(err) + return nil, err + } + + originsMap := make(map[string]int64, len(origins)) + for _, origin := range origins { + originsMap[origin.Name] = origin.Id + } + + return originsMap, nil } diff --git a/internal/user/handler.go b/internal/user/handler.go index 1e47646..b027b24 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -1,7 +1,7 @@ package user import ( - "database/sql" + "github.com/jackc/pgx/v5/pgxpool" "merch-api/pkg/utils" ) @@ -10,7 +10,7 @@ type Handler struct { } type Deps struct { - DB *sql.DB + DB *pgxpool.Pool Utils utils.Utils } diff --git a/internal/user/interface.go b/internal/user/interface.go index 0c32bbf..7050693 100644 --- a/internal/user/interface.go +++ b/internal/user/interface.go @@ -1,5 +1,7 @@ package user +import "context" + type Provider interface { - GetUserId(userUuid string) (string, error) + GetUserId(ctx context.Context, userUuid string) (string, error) } diff --git a/internal/user/repository.go b/internal/user/repository.go index c2897ae..9ff9502 100644 --- a/internal/user/repository.go +++ b/internal/user/repository.go @@ -1,24 +1,27 @@ package user -import "database/sql" +import ( + "context" + "github.com/jackc/pgx/v5/pgxpool" +) type Repository interface { - getUserId(userUuid string) (string, error) + getUserId(ctx context.Context, userUuid string) (string, error) } type repo struct { - db *sql.DB + db *pgxpool.Pool } -func newRepository(db *sql.DB) Repository { +func newRepository(db *pgxpool.Pool) Repository { return &repo{ db: db, } } -func (r *repo) getUserId(userUuid string) (string, error) { +func (r *repo) getUserId(ctx context.Context, userUuid string) (string, error) { q := `SELECT id FROM users WHERE uuid = $1 AND deleted_at IS NULL LIMIT 1` - row := r.db.QueryRow(q, userUuid) + row := r.db.QueryRow(ctx, q, userUuid) var id string if err := row.Scan(&id); err != nil { diff --git a/internal/user/service.go b/internal/user/service.go index 8471b04..5959817 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -1,6 +1,7 @@ package user import ( + "context" "errors" "merch-api/pkg/utils" ) @@ -17,10 +18,10 @@ func newService(repo Repository, utils utils.Utils) *service { } } -func (s *service) GetUserId(userUuid string) (string, error) { +func (s *service) GetUserId(ctx context.Context, userUuid string) (string, error) { if userUuid == "" { return "", errors.New("user uuid is empty") } - return s.repo.getUserId(userUuid) + return s.repo.getUserId(ctx, userUuid) } diff --git a/pkg/dbase/handler.go b/pkg/dbase/handler.go index 8f65482..4a805b4 100644 --- a/pkg/dbase/handler.go +++ b/pkg/dbase/handler.go @@ -1,9 +1,10 @@ package dbase import ( - "database/sql" + "context" "fmt" - _ "github.com/lib/pq" + _ "github.com/jackc/pgx" + "github.com/jackc/pgx/v5/pgxpool" ) type Deps struct { @@ -14,20 +15,24 @@ type Deps struct { DBName string } -func Connect(deps Deps) (*sql.DB, error) { - dsn := fmt.Sprintf( - "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", +func ConnectPool(ctx context.Context, deps Deps) (*pgxpool.Pool, error) { + connString := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s", deps.Host, deps.Port, deps.Username, deps.Password, deps.DBName) - db, err := sql.Open("postgres", dsn) + config, err := pgxpool.ParseConfig(connString) if err != nil { return nil, err } - if err = db.Ping(); err != nil { - db.Close() - return nil, fmt.Errorf("failed to connect to DB: %w", err) + pool, err := pgxpool.NewWithConfig(ctx, config) + if err != nil { + return nil, err } - return db, nil + if err = pool.Ping(ctx); err != nil { + pool.Close() + return nil, err + } + + return pool, nil }