From a5707c8116fed7b0a4f80ee4f69d9228a47a93c0 Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Thu, 19 Jun 2025 23:11:36 +0530 Subject: [PATCH 1/8] Initial setup Signed-off-by: Rajdeep Mandal --- go-fiber/Dockerfile | 16 ++++++ go-fiber/README.md | 78 ++++++++++++++++++++++++++++++ go-fiber/docker-compose.yml | 52 ++++++++++++++++++++ go-fiber/go.mod | 25 ++++++++++ go-fiber/migration/001_initial.sql | 55 +++++++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 go-fiber/Dockerfile create mode 100644 go-fiber/README.md create mode 100644 go-fiber/docker-compose.yml create mode 100644 go-fiber/go.mod create mode 100644 go-fiber/migration/001_initial.sql diff --git a/go-fiber/Dockerfile b/go-fiber/Dockerfile new file mode 100644 index 00000000..9692af8f --- /dev/null +++ b/go-fiber/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.21-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/server + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ + +COPY --from=builder /app/main . +EXPOSE 8080 +CMD ["./main"] \ No newline at end of file diff --git a/go-fiber/README.md b/go-fiber/README.md new file mode 100644 index 00000000..52c61849 --- /dev/null +++ b/go-fiber/README.md @@ -0,0 +1,78 @@ +# Product Management API + +A high-performance REST API built with Go Fiber, PostgreSQL, and Redis for managing products with advanced features like ratings, tags, analytics, and shopping carts. + +## Features + +- **Product Management**: CRUD operations for products with metadata support +- **Rating System**: Rate products and get rating summaries +- **Tagging System**: Add tags to products and search by tags +- **Shopping Cart**: Redis-based cart management +- **Analytics**: Activity logging, visitor tracking, and leaderboards +- **Performance**: PostgreSQL for persistence, Redis for caching and analytics + +## Quick Start + +### Using Docker Compose + +1. Clone the repository +2. Run: `docker-compose up -d` +3. API will be available at http://localhost:8080 + +### Manual Setup + +1. Install dependencies: `go mod tidy` +2. Set environment variables: + ```bash + export DATABASE_URL="postgres://user:pass@localhost/dbname?sslmode=disable" + export REDIS_URL="redis://localhost:6379" + export PORT="8080" + ``` +3. Run: `go run cmd/server/main.go` + +## API Endpoints + +### Products +- `GET /api/v1/products` - List all products +- `POST /api/v1/products` - Create a product +- `GET /api/v1/products/:id` - Get a product +- `PUT /api/v1/products/:id` - Update a product +- `DELETE /api/v1/products/:id` - Delete a product +- `POST /api/v1/products/bulk` - Bulk create products + +### Ratings +- `POST /api/v1/products/:id/rate` - Rate a product +- `GET /api/v1/products/:id/ratings` - Get product ratings + +### Tags +- `POST /api/v1/products/:id/tags` - Add tags to product +- `GET /api/v1/products/:id/tags` - Get product tags +- `GET /api/v1/tags/:tag/products` - Get products by tag + +### Cart +- `POST /api/v1/carts/:userId` - Update user cart +- `GET /api/v1/carts/:userId` - Get user cart + +### Analytics +- `GET /api/v1/activity` - Get activity log +- `GET /api/v1/products/:id/visitors` - Get visitor count +- `GET /api/v1/leaderboard` - Get sales leaderboard + +### Health +- `GET /health` - Health check + +## Architecture + +- **Clean Architecture**: Separation of concerns with handlers, services, and repositories +- **PostgreSQL**: Primary database for persistent data +- **Redis**: Caching, sessions, analytics, and real-time features +- **Fiber**: High-performance HTTP framework +- **UUID**: For all entity identifiers + +## Performance Features + +- Connection pooling for both PostgreSQL and Redis +- Prepared statements for database queries +- Pipeline operations for Redis bulk operations +- Proper indexing on frequently queried columns +- Efficient JSON handling for metadata \ No newline at end of file diff --git a/go-fiber/docker-compose.yml b/go-fiber/docker-compose.yml new file mode 100644 index 00000000..9befc071 --- /dev/null +++ b/go-fiber/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: productuser + POSTGRES_PASSWORD: productpass + POSTGRES_DB: productdb + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./migrations:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U productuser -d productdb"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + app: + build: . + ports: + - "8080:8080" + environment: + - PORT=8080 + - DATABASE_URL=postgres://productuser:productpass@postgres:5432/productdb?sslmode=disable + - REDIS_URL=redis://redis:6379 + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - .:/app + working_dir: /app + +volumes: + postgres_data: + redis_data: \ No newline at end of file diff --git a/go-fiber/go.mod b/go-fiber/go.mod new file mode 100644 index 00000000..f53aabe1 --- /dev/null +++ b/go-fiber/go.mod @@ -0,0 +1,25 @@ +module your-project + +go 1.21 + +require ( + github.com/gofiber/fiber/v2 v2.52.0 + github.com/google/uuid v1.3.0 + github.com/lib/pq v1.10.9 + github.com/redis/go-redis/v9 v9.3.0 +) + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) \ No newline at end of file diff --git a/go-fiber/migration/001_initial.sql b/go-fiber/migration/001_initial.sql new file mode 100644 index 00000000..9dfc1455 --- /dev/null +++ b/go-fiber/migration/001_initial.sql @@ -0,0 +1,55 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Products table +CREATE TABLE IF NOT EXISTS products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + price DECIMAL(10,2) NOT NULL CHECK (price >= 0), + quantity INTEGER NOT NULL CHECK (quantity >= 0), + metadata JSONB DEFAULT '{}', + related_ids JSONB DEFAULT '[]', + categories JSONB DEFAULT '[]', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Product ratings table +CREATE TABLE IF NOT EXISTS product_ratings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE, + score DECIMAL(2,1) NOT NULL CHECK (score >= 1 AND score <= 5), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Product tags table +CREATE TABLE IF NOT EXISTS product_tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE, + tag VARCHAR(100) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(product_id, tag) +); + +-- Indexes for better performance +CREATE INDEX IF NOT EXISTS idx_products_name ON products(name); +CREATE INDEX IF NOT EXISTS idx_products_price ON products(price); +CREATE INDEX IF NOT EXISTS idx_products_created_at ON products(created_at); +CREATE INDEX IF NOT EXISTS idx_product_ratings_product_id ON product_ratings(product_id); +CREATE INDEX IF NOT EXISTS idx_product_ratings_score ON product_ratings(score); +CREATE INDEX IF NOT EXISTS idx_product_tags_tag ON product_tags(tag); +CREATE INDEX IF NOT EXISTS idx_product_tags_product_id ON product_tags(product_id); + +-- Function to update the updated_at column +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$ language 'plpgsql'; + +-- Trigger to automatically update the updated_at column +CREATE TRIGGER update_products_updated_at + BEFORE UPDATE ON products + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); \ No newline at end of file From ee96a95a38c4c6c9785541eae02e818ea0629493 Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Fri, 20 Jun 2025 00:18:25 +0530 Subject: [PATCH 2/8] internal config Signed-off-by: Rajdeep Mandal --- go-fiber/cmd/server/main.go | 69 ++++++++++++++++++++++++++++++ go-fiber/internal/config/config.go | 26 +++++++++++ 2 files changed, 95 insertions(+) create mode 100644 go-fiber/cmd/server/main.go create mode 100644 go-fiber/internal/config/config.go diff --git a/go-fiber/cmd/server/main.go b/go-fiber/cmd/server/main.go new file mode 100644 index 00000000..2474fd92 --- /dev/null +++ b/go-fiber/cmd/server/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/recover" + + "your-project/internal/config" + "your-project/internal/database" + "your-project/internal/routes" +) + +func main() { + // Load configuration + cfg := config.Load() + + // Initialize databases + db, err := database.InitPostgres(cfg.DatabaseURL) + if err != nil { + log.Fatal("Failed to connect to PostgreSQL:", err) + } + defer db.Close() + + rdb, err := database.InitRedis(cfg.RedisURL) + if err != nil { + log.Fatal("Failed to connect to Redis:", err) + } + defer rdb.Close() + + // Create Fiber app + app := fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + if e, ok := err.(*fiber.Error); ok { + code = e.Code + } + return c.Status(code).JSON(fiber.Map{ + "error": err.Error(), + }) + }, + }) + + // Middleware + app.Use(logger.New()) + app.Use(recover.New()) + app.Use(cors.New()) + + // Setup routes + routes.Setup(app, db, rdb) + + // Graceful shutdown + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + log.Println("Gracefully shutting down...") + app.Shutdown() + }() + + log.Printf("Server starting on port %s", cfg.Port) + log.Fatal(app.Listen(":" + cfg.Port)) +} \ No newline at end of file diff --git a/go-fiber/internal/config/config.go b/go-fiber/internal/config/config.go new file mode 100644 index 00000000..68c33cc4 --- /dev/null +++ b/go-fiber/internal/config/config.go @@ -0,0 +1,26 @@ +package config + +import ( + "os" +) + +type Config struct { + Port string + DatabaseURL string + RedisURL string +} + +func Load() *Config { + return &Config{ + Port: getEnv("PORT", "8080"), + DatabaseURL: getEnv("DATABASE_URL", "postgres://user:password@localhost/dbname?sslmode=disable"), + RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"), + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} \ No newline at end of file From 1725a4e44ccf351c743deba12224404119f6e73e Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Fri, 20 Jun 2025 00:57:32 +0530 Subject: [PATCH 3/8] Database Signed-off-by: Rajdeep Mandal --- go-fiber/internal/database/postgres.go | 63 ++++++++++++++++++++++++++ go-fiber/internal/database/redis.go | 24 ++++++++++ 2 files changed, 87 insertions(+) create mode 100644 go-fiber/internal/database/postgres.go create mode 100644 go-fiber/internal/database/redis.go diff --git a/go-fiber/internal/database/postgres.go b/go-fiber/internal/database/postgres.go new file mode 100644 index 00000000..a7c15988 --- /dev/null +++ b/go-fiber/internal/database/postgres.go @@ -0,0 +1,63 @@ +package database + +import ( + "database/sql" + "fmt" + + _ "github.com/lib/pq" +) + +func InitPostgres(databaseURL string) (*sql.DB, error) { + db, err := sql.Open("postgres", databaseURL) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + // Run migrations + if err := runMigrations(db); err != nil { + return nil, fmt.Errorf("failed to run migrations: %w", err) + } + + return db, nil +} + +func runMigrations(db *sql.DB) error { + query := ` + CREATE TABLE IF NOT EXISTS products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + price DECIMAL(10,2) NOT NULL, + quantity INTEGER NOT NULL, + metadata JSONB, + related_ids JSONB, + categories JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS product_ratings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id UUID REFERENCES products(id) ON DELETE CASCADE, + score DECIMAL(2,1) NOT NULL CHECK (score >= 1 AND score <= 5), + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS product_tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id UUID REFERENCES products(id) ON DELETE CASCADE, + tag VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(product_id, tag) + ); + + CREATE INDEX IF NOT EXISTS idx_product_tags_tag ON product_tags(tag); + CREATE INDEX IF NOT EXISTS idx_product_ratings_product_id ON product_ratings(product_id); + ` + + _, err := db.Exec(query) + return err +} \ No newline at end of file diff --git a/go-fiber/internal/database/redis.go b/go-fiber/internal/database/redis.go new file mode 100644 index 00000000..459a0dca --- /dev/null +++ b/go-fiber/internal/database/redis.go @@ -0,0 +1,24 @@ +package database + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +func InitRedis(redisURL string) (*redis.Client, error) { + opt, err := redis.ParseURL(redisURL) + if err != nil { + return nil, fmt.Errorf("failed to parse redis URL: %w", err) + } + + rdb := redis.NewClient(opt) + + ctx := context.Background() + if err := rdb.Ping(ctx).Err(); err != nil { + return nil, fmt.Errorf("failed to ping redis: %w", err) + } + + return rdb, nil +} \ No newline at end of file From ddc06aeb77316fc32d11ad4826c15bf6bb1e3978 Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Fri, 20 Jun 2025 01:11:49 +0530 Subject: [PATCH 4/8] cart, prod,rating and tag repos Signed-off-by: Rajdeep Mandal --- go-fiber/internal/repository/cart.go | 76 +++++++++++ go-fiber/internal/repository/product.go | 169 ++++++++++++++++++++++++ go-fiber/internal/repository/rating.go | 66 +++++++++ go-fiber/internal/repository/tag.go | 106 +++++++++++++++ 4 files changed, 417 insertions(+) create mode 100644 go-fiber/internal/repository/cart.go create mode 100644 go-fiber/internal/repository/product.go create mode 100644 go-fiber/internal/repository/rating.go create mode 100644 go-fiber/internal/repository/tag.go diff --git a/go-fiber/internal/repository/cart.go b/go-fiber/internal/repository/cart.go new file mode 100644 index 00000000..7c9a2ba7 --- /dev/null +++ b/go-fiber/internal/repository/cart.go @@ -0,0 +1,76 @@ +package repository + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/redis/go-redis/v9" +) + +type CartRepository struct { + rdb *redis.Client +} + +func NewCartRepository(rdb *redis.Client) *CartRepository { + return &CartRepository{rdb: rdb} +} + +func (r *CartRepository) Update(ctx context.Context, userID string, cart map[string]int) error { + key := "cart:" + userID + + pipe := r.rdb.Pipeline() + pipe.Del(ctx, key) + + for productID, quantity := range cart { + pipe.HSet(ctx, key, productID, quantity) + } + + _, err := pipe.Exec(ctx) + return err +} + +func (r *CartRepository) Get(ctx context.Context, userID string) (map[string]int, error) { + key := "cart:" + userID + result, err := r.rdb.HGetAll(ctx, key).Result() + if err != nil { + return nil, err + } + + cart := make(map[string]int) + for productID, quantityStr := range result { + quantity, _ := strconv.Atoi(quantityStr) + cart[productID] = quantity + } + + return cart, nil +} + +func (r *CartRepository) LogActivity(ctx context.Context, message string) error { + return r.rdb.LPush(ctx, "activity_log", message).Err() +} + +func (r *CartRepository) GetActivityLog(ctx context.Context, limit int64) ([]string, error) { + return r.rdb.LRange(ctx, "activity_log", 0, limit-1).Result() +} + +func (r *CartRepository) IncrementVisitor(ctx context.Context, productID string, visitorID string) error { + key := "product:" + productID + ":visitors" + return r.rdb.PFAdd(ctx, key, visitorID).Err() +} + +func (r *CartRepository) GetVisitorCount(ctx context.Context, productID string) (int64, error) { + key := "product:" + productID + ":visitors" + return r.rdb.PFCount(ctx, key).Result() +} + +func (r *CartRepository) UpdateLeaderboard(ctx context.Context, productID string, sales float64) error { + return r.rdb.ZAdd(ctx, "product_leaderboard", redis.Z{ + Score: sales, + Member: productID, + }).Err() +} + +func (r *CartRepository) GetLeaderboard(ctx context.Context, limit int64) ([]redis.Z, error) { + return r.rdb.ZRevRangeWithScores(ctx, "product_leaderboard", 0, limit-1).Result() +} \ No newline at end of file diff --git a/go-fiber/internal/repository/product.go b/go-fiber/internal/repository/product.go new file mode 100644 index 00000000..8c5a0062 --- /dev/null +++ b/go-fiber/internal/repository/product.go @@ -0,0 +1,169 @@ +package repository + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "your-project/internal/models" +) + +type ProductRepository struct { + db *sql.DB +} + +func NewProductRepository(db *sql.DB) *ProductRepository { + return &ProductRepository{db: db} +} + +func (r *ProductRepository) Create(product *models.Product) error { + product.ID = uuid.New() + + metadataJSON, _ := json.Marshal(product.Metadata) + relatedJSON, _ := json.Marshal(product.RelatedIDs) + categoriesJSON, _ := json.Marshal(product.Categories) + + query := ` + INSERT INTO products (id, name, price, quantity, metadata, related_ids, categories) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING created_at, updated_at + ` + + return r.db.QueryRow(query, product.ID, product.Name, product.Price, + product.Quantity, metadataJSON, relatedJSON, categoriesJSON). + Scan(&product.CreatedAt, &product.UpdatedAt) +} + +func (r *ProductRepository) GetByID(id uuid.UUID) (*models.Product, error) { + product := &models.Product{} + var metadataJSON, relatedJSON, categoriesJSON []byte + + query := ` + SELECT id, name, price, quantity, metadata, related_ids, categories, created_at, updated_at + FROM products WHERE id = $1 + ` + + err := r.db.QueryRow(query, id).Scan( + &product.ID, &product.Name, &product.Price, &product.Quantity, + &metadataJSON, &relatedJSON, &categoriesJSON, + &product.CreatedAt, &product.UpdatedAt, + ) + + if err != nil { + return nil, err + } + + json.Unmarshal(metadataJSON, &product.Metadata) + json.Unmarshal(relatedJSON, &product.RelatedIDs) + json.Unmarshal(categoriesJSON, &product.Categories) + + return product, nil +} + +func (r *ProductRepository) List() ([]models.Product, error) { + query := ` + SELECT id, name, price, quantity, metadata, related_ids, categories, created_at, updated_at + FROM products ORDER BY created_at DESC + ` + + rows, err := r.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var products []models.Product + for rows.Next() { + var product models.Product + var metadataJSON, relatedJSON, categoriesJSON []byte + + err := rows.Scan( + &product.ID, &product.Name, &product.Price, &product.Quantity, + &metadataJSON, &relatedJSON, &categoriesJSON, + &product.CreatedAt, &product.UpdatedAt, + ) + if err != nil { + continue + } + + json.Unmarshal(metadataJSON, &product.Metadata) + json.Unmarshal(relatedJSON, &product.RelatedIDs) + json.Unmarshal(categoriesJSON, &product.Categories) + + products = append(products, product) + } + + return products, nil +} + +func (r *ProductRepository) Update(id uuid.UUID, product *models.Product) error { + metadataJSON, _ := json.Marshal(product.Metadata) + relatedJSON, _ := json.Marshal(product.RelatedIDs) + categoriesJSON, _ := json.Marshal(product.Categories) + + query := ` + UPDATE products + SET name = $2, price = $3, quantity = $4, metadata = $5, + related_ids = $6, categories = $7, updated_at = NOW() + WHERE id = $1 + RETURNING updated_at + ` + + return r.db.QueryRow(query, id, product.Name, product.Price, + product.Quantity, metadataJSON, relatedJSON, categoriesJSON). + Scan(&product.UpdatedAt) +} + +func (r *ProductRepository) Delete(id uuid.UUID) error { + query := `DELETE FROM products WHERE id = $1` + result, err := r.db.Exec(query, id) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return sql.ErrNoRows + } + + return nil +} + +func (r *ProductRepository) BulkCreate(products []models.Product) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + query := ` + INSERT INTO products (id, name, price, quantity, metadata, related_ids, categories) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ` + + stmt, err := tx.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + for i := range products { + products[i].ID = uuid.New() + metadataJSON, _ := json.Marshal(products[i].Metadata) + relatedJSON, _ := json.Marshal(products[i].RelatedIDs) + categoriesJSON, _ := json.Marshal(products[i].Categories) + + _, err := stmt.Exec(products[i].ID, products[i].Name, products[i].Price, + products[i].Quantity, metadataJSON, relatedJSON, categoriesJSON) + if err != nil { + return err + } + } + + return tx.Commit() +} \ No newline at end of file diff --git a/go-fiber/internal/repository/rating.go b/go-fiber/internal/repository/rating.go new file mode 100644 index 00000000..0f12ac21 --- /dev/null +++ b/go-fiber/internal/repository/rating.go @@ -0,0 +1,66 @@ +package repository + +import ( + "database/sql" + + "github.com/google/uuid" + "your-project/internal/models" +) + +type RatingRepository struct { + db *sql.DB +} + +func NewRatingRepository(db *sql.DB) *RatingRepository { + return &RatingRepository{db: db} +} + +func (r *RatingRepository) Create(productID uuid.UUID, score float64) error { + query := ` + INSERT INTO product_ratings (product_id, score) + VALUES ($1, $2) + ` + _, err := r.db.Exec(query, productID, score) + return err +} + +func (r *RatingRepository) GetByProductID(productID uuid.UUID) ([]models.ProductRating, error) { + query := ` + SELECT id, product_id, score, created_at + FROM product_ratings + WHERE product_id = $1 + ORDER BY created_at DESC + LIMIT 10 + ` + + rows, err := r.db.Query(query, productID) + if err != nil { + return nil, err + } + defer rows.Close() + + var ratings []models.ProductRating + for rows.Next() { + var rating models.ProductRating + err := rows.Scan(&rating.ID, &rating.ProductID, &rating.Score, &rating.CreatedAt) + if err != nil { + continue + } + ratings = append(ratings, rating) + } + + return ratings, nil +} + +func (r *RatingRepository) GetSummary(productID uuid.UUID) (float64, int, error) { + query := ` + SELECT COALESCE(AVG(score), 0), COUNT(*) + FROM product_ratings + WHERE product_id = $1 + ` + + var avg float64 + var count int + err := r.db.QueryRow(query, productID).Scan(&avg, &count) + return avg, count, err +} \ No newline at end of file diff --git a/go-fiber/internal/repository/tag.go b/go-fiber/internal/repository/tag.go new file mode 100644 index 00000000..c6a4d5d3 --- /dev/null +++ b/go-fiber/internal/repository/tag.go @@ -0,0 +1,106 @@ +package repository + +import ( + "database/sql" + + "github.com/google/uuid" + "your-project/internal/models" +) + +type TagRepository struct { + db *sql.DB +} + +func NewTagRepository(db *sql.DB) *TagRepository { + return &TagRepository{db: db} +} + +func (r *TagRepository) AddTags(productID uuid.UUID, tags []string) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + query := ` + INSERT INTO product_tags (product_id, tag) + VALUES ($1, $2) + ON CONFLICT (product_id, tag) DO NOTHING + ` + + stmt, err := tx.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + for _, tag := range tags { + _, err := stmt.Exec(productID, tag) + if err != nil { + return err + } + } + + return tx.Commit() +} + +func (r *TagRepository) GetByProductID(productID uuid.UUID) ([]string, error) { + query := ` + SELECT tag FROM product_tags + WHERE product_id = $1 + ORDER BY created_at DESC + ` + + rows, err := r.db.Query(query, productID) + if err != nil { + return nil, err + } + defer rows.Close() + + var tags []string + for rows.Next() { + var tag string + if err := rows.Scan(&tag); err != nil { + continue + } + tags = append(tags, tag) + } + + return tags, nil +} + +func (r *TagRepository) GetProductsByTag(tag string) ([]models.Product, error) { + query := ` + SELECT p.id, p.name, p.price, p.quantity, p.metadata, p.related_ids, p.categories, p.created_at, p.updated_at + FROM products p + INNER JOIN product_tags pt ON p.id = pt.product_id + WHERE pt.tag = $1 + ORDER BY p.created_at DESC + ` + + rows, err := r.db.Query(query, tag) + if err != nil { + return nil, err + } + defer rows.Close() + + var products []models.Product + for rows.Next() { + var product models.Product + var metadataJSON, relatedJSON, categoriesJSON []byte + + err := rows.Scan( + &product.ID, &product.Name, &product.Price, &product.Quantity, + &metadataJSON, &relatedJSON, &categoriesJSON, + &product.CreatedAt, &product.UpdatedAt, + ) + if err != nil { + continue + } + + // Unmarshal JSON fields (implement similar to product repository) + products = append(products, product) + } + + return products, nil +} \ No newline at end of file From db72d4331aa581753fca45191f4548d12347e35f Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Fri, 20 Jun 2025 01:40:45 +0530 Subject: [PATCH 5/8] repo srevices Signed-off-by: Rajdeep Mandal --- go-fiber/internal/services/analytics.go | 69 +++++++++++++++++++++++++ go-fiber/internal/services/cart.go | 23 +++++++++ go-fiber/internal/services/product.go | 67 ++++++++++++++++++++++++ go-fiber/internal/services/rating.go | 38 ++++++++++++++ go-fiber/internal/services/tag.go | 27 ++++++++++ 5 files changed, 224 insertions(+) create mode 100644 go-fiber/internal/services/analytics.go create mode 100644 go-fiber/internal/services/cart.go create mode 100644 go-fiber/internal/services/product.go create mode 100644 go-fiber/internal/services/rating.go create mode 100644 go-fiber/internal/services/tag.go diff --git a/go-fiber/internal/services/analytics.go b/go-fiber/internal/services/analytics.go new file mode 100644 index 00000000..f7eacf1a --- /dev/null +++ b/go-fiber/internal/services/analytics.go @@ -0,0 +1,69 @@ +package services + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "your-project/internal/models" + "your-project/internal/repository" +) + +type AnalyticsService struct { + cartRepo *repository.CartRepository + productRepo *repository.ProductRepository +} + +func NewAnalyticsService(cartRepo *repository.CartRepository, productRepo *repository.ProductRepository) *AnalyticsService { + return &AnalyticsService{ + cartRepo: cartRepo, + productRepo: productRepo, + } +} + +func (s *AnalyticsService) LogActivity(ctx context.Context, message string) error { + return s.cartRepo.LogActivity(ctx, message) +} + +func (s *AnalyticsService) GetActivityLog(ctx context.Context, limit int64) ([]string, error) { + return s.cartRepo.GetActivityLog(ctx, limit) +} + +func (s *AnalyticsService) TrackVisitor(ctx context.Context, productID, visitorID string) error { + return s.cartRepo.IncrementVisitor(ctx, productID, visitorID) +} + +func (s *AnalyticsService) GetVisitorCount(ctx context.Context, productID string) (int64, error) { + return s.cartRepo.GetVisitorCount(ctx, productID) +} + +func (s *AnalyticsService) UpdateLeaderboard(ctx context.Context, productID string, sales float64) error { + return s.cartRepo.UpdateLeaderboard(ctx, productID, sales) +} + +func (s *AnalyticsService) GetLeaderboard(ctx context.Context, limit int64) ([]map[string]interface{}, error) { + results, err := s.cartRepo.GetLeaderboard(ctx, limit) + if err != nil { + return nil, err + } + + var leaderboard []map[string]interface{} + for _, result := range results { + productID, err := uuid.Parse(result.Member.(string)) + if err != nil { + continue + } + + product, err := s.productRepo.GetByID(productID) + if err != nil { + continue + } + + leaderboard = append(leaderboard, map[string]interface{}{ + "product": product, + "sales": result.Score, + }) + } + + return leaderboard, nil +} \ No newline at end of file diff --git a/go-fiber/internal/services/cart.go b/go-fiber/internal/services/cart.go new file mode 100644 index 00000000..d908bb1d --- /dev/null +++ b/go-fiber/internal/services/cart.go @@ -0,0 +1,23 @@ +package services + +import ( + "context" + + "your-project/internal/repository" +) + +type CartService struct { + repo *repository.CartRepository +} + +func NewCartService(repo *repository.CartRepository) *CartService { + return &CartService{repo: repo} +} + +func (s *CartService) UpdateCart(ctx context.Context, userID string, cart map[string]int) error { + return s.repo.Update(ctx, userID, cart) +} + +func (s *CartService) GetCart(ctx context.Context, userID string) (map[string]int, error) { + return s.repo.Get(ctx, userID) +} \ No newline at end of file diff --git a/go-fiber/internal/services/product.go b/go-fiber/internal/services/product.go new file mode 100644 index 00000000..3149eee6 --- /dev/null +++ b/go-fiber/internal/services/product.go @@ -0,0 +1,67 @@ +package services + +import ( + "database/sql" + + "github.com/google/uuid" + "your-project/internal/models" + "your-project/internal/repository" +) + +type ProductService struct { + repo *repository.ProductRepository +} + +func NewProductService(repo *repository.ProductRepository) *ProductService { + return &ProductService{repo: repo} +} + +func (s *ProductService) CreateProduct(product *models.Product) error { + return s.repo.Create(product) +} + +func (s *ProductService) GetProduct(id uuid.UUID) (*models.Product, error) { + return s.repo.GetByID(id) +} + +func (s *ProductService) ListProducts() ([]models.Product, error) { + return s.repo.List() +} + +func (s *ProductService) UpdateProduct(id uuid.UUID, product *models.Product) error { + // Check if product exists + existing, err := s.repo.GetByID(id) + if err != nil { + return err + } + + // Update only non-zero fields + if product.Name != "" { + existing.Name = product.Name + } + if product.Price > 0 { + existing.Price = product.Price + } + if product.Quantity >= 0 { + existing.Quantity = product.Quantity + } + if len(product.Metadata) > 0 { + existing.Metadata = product.Metadata + } + if len(product.RelatedIDs) > 0 { + existing.RelatedIDs = product.RelatedIDs + } + if len(product.Categories) > 0 { + existing.Categories = product.Categories + } + + return s.repo.Update(id, existing) +} + +func (s *ProductService) DeleteProduct(id uuid.UUID) error { + return s.repo.Delete(id) +} + +func (s *ProductService) BulkCreateProducts(products []models.Product) error { + return s.repo.BulkCreate(products) +} \ No newline at end of file diff --git a/go-fiber/internal/services/rating.go b/go-fiber/internal/services/rating.go new file mode 100644 index 00000000..e305310b --- /dev/null +++ b/go-fiber/internal/services/rating.go @@ -0,0 +1,38 @@ +package services + +import ( + "github.com/google/uuid" + "your-project/internal/models" + "your-project/internal/repository" +) + +type RatingService struct { + repo *repository.RatingRepository +} + +func NewRatingService(repo *repository.RatingRepository) *RatingService { + return &RatingService{repo: repo} +} + +func (s *RatingService) CreateRating(productID uuid.UUID, score float64) error { + if score < 1 || score > 5 { + return fmt.Errorf("score must be between 1 and 5") + } + return s.repo.Create(productID, score) +} + +func (s *RatingService) GetRatingsByProduct(productID uuid.UUID) ([]models.ProductRating, error) { + return s.repo.GetByProductID(productID) +} + +func (s *RatingService) GetRatingSummary(productID uuid.UUID) (map[string]interface{}, error) { + avg, count, err := s.repo.GetSummary(productID) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "average_rating": avg, + "total_ratings": count, + }, nil +} \ No newline at end of file diff --git a/go-fiber/internal/services/tag.go b/go-fiber/internal/services/tag.go new file mode 100644 index 00000000..38600f05 --- /dev/null +++ b/go-fiber/internal/services/tag.go @@ -0,0 +1,27 @@ +package services + +import ( + "github.com/google/uuid" + "your-project/internal/models" + "your-project/internal/repository" +) + +type TagService struct { + repo *repository.TagRepository +} + +func NewTagService(repo *repository.TagRepository) *TagService { + return &TagService{repo: repo} +} + +func (s *TagService) AddTags(productID uuid.UUID, tags []string) error { + return s.repo.AddTags(productID, tags) +} + +func (s *TagService) GetTagsByProduct(productID uuid.UUID) ([]string, error) { + return s.repo.GetByProductID(productID) +} + +func (s *TagService) GetProductsByTag(tag string) ([]models.Product, error) { + return s.repo.GetProductsByTag(tag) +} \ No newline at end of file From 894a23b300d5a1625dd32f3a5ae5c6f78c91f48a Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Fri, 20 Jun 2025 01:46:41 +0530 Subject: [PATCH 6/8] model prod Signed-off-by: Rajdeep Mandal --- go-fiber/internal/models/products.go | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 go-fiber/internal/models/products.go diff --git a/go-fiber/internal/models/products.go b/go-fiber/internal/models/products.go new file mode 100644 index 00000000..faac2ee7 --- /dev/null +++ b/go-fiber/internal/models/products.go @@ -0,0 +1,65 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "github.com/google/uuid" +) + +type Product struct { + ID uuid.UUID `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Price float64 `json:"price" db:"price"` + Quantity int `json:"quantity" db:"quantity"` + Metadata map[string]interface{} `json:"metadata,omitempty" db:"metadata"` + RelatedIDs []string `json:"related_ids,omitempty" db:"related_ids"` + Categories []string `json:"categories,omitempty" db:"categories"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type ProductRating struct { + ID uuid.UUID `json:"id" db:"id"` + ProductID uuid.UUID `json:"product_id" db:"product_id"` + Score float64 `json:"score" db:"score"` + CreatedAt time.Time `json:"created_at" db:"created_at"` +} + +type ProductTag struct { + ID uuid.UUID `json:"id" db:"id"` + ProductID uuid.UUID `json:"product_id" db:"product_id"` + Tag string `json:"tag" db:"tag"` + CreatedAt time.Time `json:"created_at" db:"created_at"` +} + +// JSONB implements the driver.Valuer and sql.Scanner interfaces for PostgreSQL JSONB +type JSONB map[string]interface{} + +func (j JSONB) Value() (driver.Value, error) { + return json.Marshal(j) +} + +func (j *JSONB) Scan(value interface{}) error { + if value == nil { + *j = make(map[string]interface{}) + return nil + } + return json.Unmarshal(value.([]byte), j) +} + +// StringSlice implements the driver.Valuer and sql.Scanner interfaces for string slices +type StringSlice []string + +func (s StringSlice) Value() (driver.Value, error) { + return json.Marshal(s) +} + +func (s *StringSlice) Scan(value interface{}) error { + if value == nil { + *s = []string{} + return nil + } + return json.Unmarshal(value.([]byte), s) +} \ No newline at end of file From eb17f62c82d04db7a8c2515ba0a0004dc50fdffd Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Fri, 20 Jun 2025 01:47:27 +0530 Subject: [PATCH 7/8] Card nd prod handlers Signed-off-by: Rajdeep Mandal --- go-fiber/internal/handlers/cart.go | 48 +++++++++ go-fiber/internal/handlers/product.go | 144 ++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 go-fiber/internal/handlers/cart.go create mode 100644 go-fiber/internal/handlers/product.go diff --git a/go-fiber/internal/handlers/cart.go b/go-fiber/internal/handlers/cart.go new file mode 100644 index 00000000..3e258ab1 --- /dev/null +++ b/go-fiber/internal/handlers/cart.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + "your-project/internal/services" +) + +type CartHandler struct { + service *services.CartService +} + +func NewCartHandler(service *services.CartService) *CartHandler { + return &CartHandler{service: service} +} + +func (h *CartHandler) Update(c *fiber.Ctx) error { + userID := c.Params("userId") + + var cart map[string]int + if err := c.BodyParser(&cart); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + if err := h.service.UpdateCart(c.Context(), userID, cart); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "message": "Cart updated successfully", + }) +} + +func (h *CartHandler) Get(c *fiber.Ctx) error { + userID := c.Params("userId") + + cart, err := h.service.GetCart(c.Context(), userID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(cart) +} \ No newline at end of file diff --git a/go-fiber/internal/handlers/product.go b/go-fiber/internal/handlers/product.go new file mode 100644 index 00000000..652d8810 --- /dev/null +++ b/go-fiber/internal/handlers/product.go @@ -0,0 +1,144 @@ +package handlers + +import ( + "database/sql" + + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "your-project/internal/models" + "your-project/internal/services" +) + +type ProductHandler struct { + service *services.ProductService +} + +func NewProductHandler(service *services.ProductService) *ProductHandler { + return &ProductHandler{service: service} +} + +func (h *ProductHandler) List(c *fiber.Ctx) error { + products, err := h.service.ListProducts() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + return c.JSON(products) +} + +func (h *ProductHandler) Create(c *fiber.Ctx) error { + var product models.Product + if err := c.BodyParser(&product); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + if err := h.service.CreateProduct(&product); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(product) +} + +func (h *ProductHandler) Get(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + product, err := h.service.GetProduct(id) + if err != nil { + if err == sql.ErrNoRows { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "error": "Product not found", + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(product) +} + +func (h *ProductHandler) Update(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + var product models.Product + if err := c.BodyParser(&product); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + if err := h.service.UpdateProduct(id, &product); err != nil { + if err == sql.ErrNoRows { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "error": "Product not found", + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + updatedProduct, _ := h.service.GetProduct(id) + return c.JSON(updatedProduct) +} + +func (h *ProductHandler) Delete(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + if err := h.service.DeleteProduct(id); err != nil { + if err == sql.ErrNoRows { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "error": "Product not found", + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "message": "Product deleted successfully", + }) +} + +func (h *ProductHandler) BulkCreate(c *fiber.Ctx) error { + var products []models.Product + if err := c.BodyParser(&products); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + if err := h.service.BulkCreateProducts(products); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(fiber.Map{ + "created": len(products), + }) +} \ No newline at end of file From 33709280cb8e4e2e7e8e275ba8e260d75361b7f0 Mon Sep 17 00:00:00 2001 From: Rajdeep Mandal Date: Fri, 20 Jun 2025 01:58:19 +0530 Subject: [PATCH 8/8] all handlers and routes Signed-off-by: Rajdeep Mandal --- go-fiber/internal/handlers/analytics.go | 79 +++++++++++++++++++++++++ go-fiber/internal/handlers/rating.go | 73 +++++++++++++++++++++++ go-fiber/internal/handlers/tag.go | 78 ++++++++++++++++++++++++ go-fiber/internal/routes/routes.go | 67 +++++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 go-fiber/internal/handlers/analytics.go create mode 100644 go-fiber/internal/handlers/rating.go create mode 100644 go-fiber/internal/handlers/tag.go create mode 100644 go-fiber/internal/routes/routes.go diff --git a/go-fiber/internal/handlers/analytics.go b/go-fiber/internal/handlers/analytics.go new file mode 100644 index 00000000..9d3820e3 --- /dev/null +++ b/go-fiber/internal/handlers/analytics.go @@ -0,0 +1,79 @@ +package handlers + +import ( + "strconv" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "your-project/internal/services" +) + +type AnalyticsHandler struct { + service *services.AnalyticsService +} + +func NewAnalyticsHandler(service *services.AnalyticsService) *AnalyticsHandler { + return &AnalyticsHandler{service: service} +} + +func (h *AnalyticsHandler) GetActivityLog(c *fiber.Ctx) error { + limit := int64(10) + if l := c.Query("limit"); l != "" { + if parsed, err := strconv.ParseInt(l, 10, 64); err == nil { + limit = parsed + } + } + + activities, err := h.service.GetActivityLog(c.Context(), limit) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "activities": activities, + }) +} + +func (h *AnalyticsHandler) GetVisitors(c *fiber.Ctx) error { + idStr := c.Params("id") + productID, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + // Track this visitor + visitorID := c.Get("X-Visitor-ID", c.IP()) + h.service.TrackVisitor(c.Context(), productID.String(), visitorID) + + count, err := h.service.GetVisitorCount(c.Context(), productID.String()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "unique_visitors": count, + }) +} + +func (h *AnalyticsHandler) GetLeaderboard(c *fiber.Ctx) error { + limit := int64(10) + if l := c.Query("limit"); l != "" { + if parsed, err := strconv.ParseInt(l, 10, 64); err == nil { + limit = parsed + } + } + + leaderboard, err := h.service.GetLeaderboard(c.Context(), limit) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(leaderboard) +} \ No newline at end of file diff --git a/go-fiber/internal/handlers/rating.go b/go-fiber/internal/handlers/rating.go new file mode 100644 index 00000000..a4db8034 --- /dev/null +++ b/go-fiber/internal/handlers/rating.go @@ -0,0 +1,73 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "your-project/internal/services" +) + +type RatingHandler struct { + service *services.RatingService +} + +func NewRatingHandler(service *services.RatingService) *RatingHandler { + return &RatingHandler{service: service} +} + +func (h *RatingHandler) Create(c *fiber.Ctx) error { + idStr := c.Params("id") + productID, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + var request struct { + Score float64 `json:"score" validate:"required,min=1,max=5"` + } + if err := c.BodyParser(&request); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + if err := h.service.CreateRating(productID, request.Score); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "message": "Rating added successfully", + }) +} + +func (h *RatingHandler) GetByProduct(c *fiber.Ctx) error { + idStr := c.Params("id") + productID, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + ratings, err := h.service.GetRatingsByProduct(productID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + summary, err := h.service.GetRatingSummary(productID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "ratings": ratings, + "summary": summary, + }) +} \ No newline at end of file diff --git a/go-fiber/internal/handlers/tag.go b/go-fiber/internal/handlers/tag.go new file mode 100644 index 00000000..371d9333 --- /dev/null +++ b/go-fiber/internal/handlers/tag.go @@ -0,0 +1,78 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "your-project/internal/services" +) + +type TagHandler struct { + service *services.TagService +} + +func NewTagHandler(service *services.TagService) *TagHandler { + return &TagHandler{service: service} +} + +func (h *TagHandler) AddTags(c *fiber.Ctx) error { + idStr := c.Params("id") + productID, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + var request struct { + Tags []string `json:"tags" validate:"required"` + } + if err := c.BodyParser(&request); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + if err := h.service.AddTags(productID, request.Tags); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "message": "Tags added successfully", + }) +} + +func (h *TagHandler) GetByProduct(c *fiber.Ctx) error { + idStr := c.Params("id") + productID, err := uuid.Parse(idStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid product ID", + }) + } + + tags, err := h.service.GetTagsByProduct(productID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "tags": tags, + }) +} + +func (h *TagHandler) GetProductsByTag(c *fiber.Ctx) error { + tag := c.Params("tag") + + products, err := h.service.GetProductsByTag(tag) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(products) +} \ No newline at end of file diff --git a/go-fiber/internal/routes/routes.go b/go-fiber/internal/routes/routes.go new file mode 100644 index 00000000..de5f239f --- /dev/null +++ b/go-fiber/internal/routes/routes.go @@ -0,0 +1,67 @@ +package routes + +import ( + "database/sql" + + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + + "your-project/internal/handlers" + "your-project/internal/repository" + "your-project/internal/services" +) + +func Setup(app *fiber.App, db *sql.DB, rdb *redis.Client) { + // Initialize repositories + productRepo := repository.NewProductRepository(db) + ratingRepo := repository.NewRatingRepository(db) + tagRepo := repository.NewTagRepository(db) + cartRepo := repository.NewCartRepository(rdb) + + // Initialize services + productService := services.NewProductService(productRepo) + // Add other services here... + + // Initialize handlers + productHandler := handlers.NewProductHandler(productService) + // Add other handlers here... + + // API routes + api := app.Group("/api/v1") + + // Product routes + products := api.Group("/products") + products.Get("/", productHandler.List) + products.Post("/", productHandler.Create) + products.Get("/:id", productHandler.Get) + products.Put("/:id", productHandler.Update) + products.Delete("/:id", productHandler.Delete) + products.Post("/bulk", productHandler.BulkCreate) + + // Rating routes (to be implemented) + // products.Post("/:id/rate", ratingHandler.Create) + // products.Get("/:id/ratings", ratingHandler.GetByProduct) + + // Tag routes (to be implemented) + // products.Post("/:id/tags", tagHandler.AddTags) + // products.Get("/:id/tags", tagHandler.GetByProduct) + // api.Get("/tags/:tag/products", tagHandler.GetProductsByTag) + + // Cart routes (to be implemented) + // api.Post("/carts/:userId", cartHandler.Update) + // api.Get("/carts/:userId", cartHandler.Get) + + // Analytics routes (to be implemented) + // api.Get("/activity", analyticsHandler.GetActivityLog) + // api.Get("/products/:id/visitors", analyticsHandler.GetVisitors) + // api.Get("/leaderboard", analyticsHandler.GetLeaderboard) + + // Health check + app.Get("/health", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "status": "ok", + "postgres": "connected", + "redis": "connected", + }) + }) +} \ No newline at end of file