190 lines
5.0 KiB
Go
190 lines
5.0 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"shop/internal/models"
|
|
)
|
|
|
|
type ProductRepository struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewProductRepository(pool *pgxpool.Pool) *ProductRepository {
|
|
return &ProductRepository{pool: pool}
|
|
}
|
|
|
|
func (r *ProductRepository) scanProduct(row pgx.Row) (models.Product, error) {
|
|
var p models.Product
|
|
err := row.Scan(
|
|
&p.ID, &p.Name, &p.Description, &p.Details, &p.Price,
|
|
&p.ImageURL, &p.Category, &p.IsActive, &p.CreatedAt, &p.UpdatedAt,
|
|
)
|
|
return p, err
|
|
}
|
|
|
|
const productColumns = `id, name, description, details, price, image_url, category, is_active, created_at, updated_at`
|
|
|
|
func (r *ProductRepository) Featured(ctx context.Context, limit int) ([]models.Product, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT `+productColumns+`
|
|
FROM products
|
|
WHERE is_active = true
|
|
ORDER BY created_at DESC
|
|
LIMIT $1
|
|
`, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return r.collectProducts(rows)
|
|
}
|
|
|
|
func (r *ProductRepository) All(ctx context.Context) ([]models.Product, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT `+productColumns+`
|
|
FROM products
|
|
ORDER BY created_at DESC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return r.collectProducts(rows)
|
|
}
|
|
|
|
func (r *ProductRepository) GetByID(ctx context.Context, id int) (models.Product, error) {
|
|
row := r.pool.QueryRow(ctx, `SELECT `+productColumns+` FROM products WHERE id = $1`, id)
|
|
p, err := r.scanProduct(row)
|
|
if err != nil {
|
|
return models.Product{}, err
|
|
}
|
|
p.Images, err = r.ImagesByProduct(ctx, id)
|
|
return p, err
|
|
}
|
|
|
|
func (r *ProductRepository) Create(ctx context.Context, p *models.Product) error {
|
|
return r.pool.QueryRow(ctx, `
|
|
INSERT INTO products (name, description, details, price, image_url, category, is_active)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, created_at, updated_at
|
|
`, p.Name, p.Description, p.Details, p.Price, p.ImageURL, p.Category, p.IsActive,
|
|
).Scan(&p.ID, &p.CreatedAt, &p.UpdatedAt)
|
|
}
|
|
|
|
func (r *ProductRepository) Update(ctx context.Context, p *models.Product) error {
|
|
tag, err := r.pool.Exec(ctx, `
|
|
UPDATE products
|
|
SET name = $1, description = $2, details = $3, price = $4,
|
|
image_url = $5, category = $6, is_active = $7, updated_at = NOW()
|
|
WHERE id = $8
|
|
`, p.Name, p.Description, p.Details, p.Price, p.ImageURL, p.Category, p.IsActive, p.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return pgx.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *ProductRepository) Delete(ctx context.Context, id int) error {
|
|
tag, err := r.pool.Exec(ctx, `DELETE FROM products WHERE id = $1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return pgx.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *ProductRepository) Categories(ctx context.Context) ([]models.Category, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT category, COUNT(*) AS cnt
|
|
FROM products
|
|
GROUP BY category
|
|
ORDER BY cnt DESC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var categories []models.Category
|
|
for rows.Next() {
|
|
var c models.Category
|
|
if err := rows.Scan(&c.Name, &c.Count); err != nil {
|
|
return nil, err
|
|
}
|
|
c.Slug = c.Name
|
|
categories = append(categories, c)
|
|
}
|
|
return categories, rows.Err()
|
|
}
|
|
|
|
func (r *ProductRepository) ImagesByProduct(ctx context.Context, productID int) ([]models.ProductImage, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id, product_id, file_path, is_primary, sort_order, created_at
|
|
FROM product_images
|
|
WHERE product_id = $1
|
|
ORDER BY is_primary DESC, sort_order ASC, id ASC
|
|
`, productID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var images []models.ProductImage
|
|
for rows.Next() {
|
|
var img models.ProductImage
|
|
if err := rows.Scan(&img.ID, &img.ProductID, &img.FilePath, &img.IsPrimary, &img.SortOrder, &img.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
images = append(images, img)
|
|
}
|
|
return images, rows.Err()
|
|
}
|
|
|
|
func (r *ProductRepository) AddImage(ctx context.Context, img *models.ProductImage) error {
|
|
return r.pool.QueryRow(ctx, `
|
|
INSERT INTO product_images (product_id, file_path, is_primary, sort_order)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, created_at
|
|
`, img.ProductID, img.FilePath, img.IsPrimary, img.SortOrder,
|
|
).Scan(&img.ID, &img.CreatedAt)
|
|
}
|
|
|
|
func (r *ProductRepository) DeleteImage(ctx context.Context, imageID, productID int) (string, error) {
|
|
var path string
|
|
err := r.pool.QueryRow(ctx, `
|
|
DELETE FROM product_images
|
|
WHERE id = $1 AND product_id = $2
|
|
RETURNING file_path
|
|
`, imageID, productID).Scan(&path)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", fmt.Errorf("image not found")
|
|
}
|
|
return path, err
|
|
}
|
|
|
|
func (r *ProductRepository) collectProducts(rows pgx.Rows) ([]models.Product, error) {
|
|
var products []models.Product
|
|
for rows.Next() {
|
|
var p models.Product
|
|
if err := rows.Scan(
|
|
&p.ID, &p.Name, &p.Description, &p.Details, &p.Price,
|
|
&p.ImageURL, &p.Category, &p.IsActive, &p.CreatedAt, &p.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
products = append(products, p)
|
|
}
|
|
return products, rows.Err()
|
|
}
|