Skip to content

Commit

Permalink
Merge pull request #3 from Aditya-Chowdhary/dev
Browse files Browse the repository at this point in the history
feat: standard error handling
  • Loading branch information
souvik03-136 authored Jul 9, 2024
2 parents f8dbddf + 59d22bc commit f4fad56
Show file tree
Hide file tree
Showing 21 changed files with 497 additions and 65 deletions.
58 changes: 58 additions & 0 deletions internal/controllers/v1/events/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package events

import (
"math/rand"
"net/http"
"spotify-collab/internal/database"
"spotify-collab/internal/merrors"

"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
)

type EventHandler struct {
db *pgxpool.Pool
}

func Handler(db *pgxpool.Pool) *EventHandler {
return &EventHandler{
db: db,
}
}

func (e *EventHandler) CreateEvent(c *gin.Context) {
req, err := validateCreateEventReq(c)
if err != nil {
merrors.Validation(c, err.Error())
return
}

q := database.New(e.db)

eventCode := GenerateEventCode(6)

event, err := q.CreateEvent(c, database.CreateEventParams{
UserUuid: req.UserUUID,
Name: req.Name,
EventCode: eventCode,
})
if err != nil {
merrors.InternalServer(c, err.Error())
}

c.JSON(http.StatusOK, CreateEventRes{
CreatedAt: event.CreatedAt,
EventUUID: event.EventUuid,
Name: event.Name,
})
}

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")

func GenerateEventCode(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
17 changes: 17 additions & 0 deletions internal/controllers/v1/events/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package events

import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)

type CreateEventReq struct {
UserUUID uuid.UUID `json:"user_uuid"`
Name string `json:"name"`
}

type CreateEventRes struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
EventUUID uuid.UUID `json:"event_uuid"`
Name string `json:"name"`
}
9 changes: 9 additions & 0 deletions internal/controllers/v1/events/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package events

import "github.com/gin-gonic/gin"

func validateCreateEventReq(c *gin.Context) (CreateEventReq, error) {
var req CreateEventReq
err := c.ShouldBindJSON(req)
return req, err
}
97 changes: 48 additions & 49 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,29 @@ package database

import (
"context"
"database/sql"
"fmt"
"log"
"os"
"strconv"
"time"

"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
_ "github.com/joho/godotenv/autoload"
)

// Service represents a service that interacts with a database.
type Service interface {
// Health returns a map of health status information.
// The keys and values in the map are service-specific.
Health() map[string]string

// Close terminates the database connection.
// It returns an error if the connection cannot be closed.
Close() error
}

type service struct {
db *sql.DB
// type Service interface {
// // Health returns a map of health status information.
// // The keys and values in the map are service-specific.
// Health() map[string]string

// // Close terminates the database connection.
// // It returns an error if the connection cannot be closed.
// Close() error
// }

type Service struct {
Db *pgxpool.Pool
}

var (
Expand All @@ -35,35 +34,34 @@ var (
port = os.Getenv("DB_PORT")
host = os.Getenv("DB_HOST")
schema = os.Getenv("DB_SCHEMA")
dbInstance *service
dbInstance *pgxpool.Pool
)

func NewService() Service {
func NewService() *pgxpool.Pool {
// Reuse Connection
if dbInstance != nil {
return dbInstance
}
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&search_path=%s", username, password, host, port, database, schema)
db, err := sql.Open("pgx", connStr)
db, err := pgxpool.New(context.Background(), connStr)
if err != nil {
log.Fatal(err)
}
dbInstance = &service{
db: db,
}
dbInstance = db
return dbInstance
}

// Health checks the health of the database connection by pinging the database.
// It returns a map with keys indicating various health statistics.
func (s *service) Health() map[string]string {
func (s *Service) Health() map[string]string {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

stats := make(map[string]string)

// Ping the database
err := s.db.PingContext(ctx)
// err := s.db.PingContext(ctx)
err := s.Db.Ping(ctx)
if err != nil {
stats["status"] = "down"
stats["error"] = fmt.Sprintf("db down: %v", err)
Expand All @@ -76,31 +74,32 @@ func (s *service) Health() map[string]string {
stats["message"] = "It's healthy"

// Get database stats (like open connections, in use, idle, etc.)
dbStats := s.db.Stats()
stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections)
stats["in_use"] = strconv.Itoa(dbStats.InUse)
stats["idle"] = strconv.Itoa(dbStats.Idle)
stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10)
stats["wait_duration"] = dbStats.WaitDuration.String()
stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10)
stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10)

// Evaluate stats to provide a health message
if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example
stats["message"] = "The database is experiencing heavy load."
}

if dbStats.WaitCount > 1000 {
stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks."
}

if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 {
stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings."
}

if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 {
stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern."
}
// dbStats := s.db.Stats()
_ = s.Db.Stat()
// stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections)
// stats["in_use"] = strconv.Itoa(dbStats.InUse)
// stats["idle"] = strconv.Itoa(dbStats.Idle)
// stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10)
// stats["wait_duration"] = dbStats.WaitDuration.String()
// stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10)
// stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10)

// // Evaluate stats to provide a health message
// if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example
// stats["message"] = "The database is experiencing heavy load."
// }

// if dbStats.WaitCount > 1000 {
// stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks."
// }

// if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 {
// stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings."
// }

// if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 {
// stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern."
// }

return stats
}
Expand All @@ -109,7 +108,7 @@ func (s *service) Health() map[string]string {
// It logs a message indicating the disconnection from the specific database.
// If the connection is successfully closed, it returns nil.
// If an error occurs while closing the connection, it returns the error.
func (s *service) Close() error {
func (s *Service) Close() {
log.Printf("Disconnected from database: %s", database)
return s.db.Close()
s.Db.Close()
}
25 changes: 16 additions & 9 deletions internal/database/events.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ CREATE TABLE public.playlists (
created_at timestamptz DEFAULT now() NOT NULL,
updated_at timestamptz DEFAULT now() NOT NULL,
CONSTRAINT playlists_pk PRIMARY KEY (playlist_id),
CONSTRAINT playlists_unique UNIQUE (name),
CONSTRAINT playlists_unique UNIQUE (event_uuid, name),
CONSTRAINT playlists_events_fk FOREIGN KEY (event_uuid) REFERENCES public.events(event_uuid) ON DELETE CASCADE ON UPDATE CASCADE
);
15 changes: 15 additions & 0 deletions internal/database/playlists.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions internal/database/queries/events.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- name: CreateEvent :one
INSERT INTO events (user_uuid, event_uuid, name, event_code)
VALUES ($1, $2, $3, $4)
INSERT INTO events (user_uuid, name, event_code)
VALUES ($1, $2, $3)
RETURNING *;

-- name: GetAllEvents :many
Expand All @@ -18,6 +18,11 @@ SELECT event_uuid
FROM events
WHERE name = $1;

-- name: GetEventUUIDByCode :one
Select event_uuid
FROM events
WHERE event_code = $1;

-- name: UpdateEventName :one
UPDATE events
SET name = $1
Expand Down
8 changes: 7 additions & 1 deletion internal/database/queries/playlists.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ SELECT *
FROM playlists
WHERE event_uuid = $1 AND playlist_id = $2;


-- name: GetPlaylistUUIDByName :one
SELECT playlist_id
FROM playlists
WHERE event_uuid = $1 AND name = $2;

-- name: GetPlaylistUUIDByEventUUID :one
Select playlist_id
FROM playlists
WHERE event_uuid = $1
ORDER BY created_at desc
Limit 1;

-- name: UpdatePlaylistName :one
UPDATE playlists
SET name = $1
Expand Down
25 changes: 25 additions & 0 deletions internal/merrors/conflict_409.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package merrors

import (
"net/http"

"spotify-collab/internal/utils"

"github.com/gin-gonic/gin"
)

/* -------------------------------------------------------------------------- */
/* Conflict 409 */
/* -------------------------------------------------------------------------- */

func Conflict(ctx *gin.Context, err string) {
var res utils.BaseResponse
var smerror utils.Error
errorCode := http.StatusConflict
smerror.Code = errorCode
smerror.Type = errorType.conflict
smerror.Message = err
res.Error = smerror
ctx.JSON(errorCode, res)
ctx.Abort()
}
Loading

0 comments on commit f4fad56

Please sign in to comment.