switch to pgx driver + create merch

This commit is contained in:
nquidox 2026-03-04 17:02:11 +03:00
parent 546fe13107
commit 97f8d27430
13 changed files with 289 additions and 69 deletions

View file

@ -2,13 +2,14 @@ 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
@ -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)

View file

@ -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,
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)
}

View file

@ -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

View file

@ -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"`
}

View file

@ -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
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,

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
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,
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
}

View file

@ -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
}

View file

@ -1,5 +1,7 @@
package user
import "context"
type Provider interface {
GetUserId(userUuid string) (string, error)
GetUserId(ctx context.Context, userUuid string) (string, error)
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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
}