From 350ba468e7ade05557713e145d70e802239b2d7f Mon Sep 17 00:00:00 2001 From: Aditya-Chowdhary Date: Tue, 9 Jul 2024 17:24:10 +0530 Subject: [PATCH 1/3] feat: endpoints --- internal/database/database.go | 102 +++++++++--------- internal/database/events.sql.go | 25 +++-- .../000003_create_playlists_table.up.sql | 2 +- internal/database/playlists.sql.go | 15 +++ internal/database/queries/events.sql | 9 +- internal/database/queries/playlists.sql | 8 +- internal/server/routes.go | 101 +++++++++++++++++ 7 files changed, 199 insertions(+), 63 deletions(-) diff --git a/internal/database/database.go b/internal/database/database.go index 22c65c9..9a2eed3 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -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 ( @@ -35,35 +34,37 @@ var ( port = os.Getenv("DB_PORT") host = os.Getenv("DB_HOST") schema = os.Getenv("DB_SCHEMA") - dbInstance *service + dbInstance *Service ) func NewService() Service { // Reuse Connection - if dbInstance != nil { - return dbInstance + if dbInstance.Db != 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 := sql.Open("pgx", connStr) + db, err := pgxpool.New(context.Background(), connStr) if err != nil { log.Fatal(err) } - dbInstance = &service{ - db: db, + dbInstance = &Service{ + Db: db, } - return dbInstance + 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) @@ -76,31 +77,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 } @@ -109,7 +111,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() } diff --git a/internal/database/events.sql.go b/internal/database/events.sql.go index 6b979f1..c065cd9 100644 --- a/internal/database/events.sql.go +++ b/internal/database/events.sql.go @@ -12,25 +12,19 @@ import ( ) const createEvent = `-- 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 user_uuid, event_uuid, created_at, updated_at, name, event_code ` type CreateEventParams struct { UserUuid uuid.UUID `json:"user_uuid"` - EventUuid uuid.UUID `json:"event_uuid"` Name string `json:"name"` EventCode string `json:"event_code"` } func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { - row := q.db.QueryRow(ctx, createEvent, - arg.UserUuid, - arg.EventUuid, - arg.Name, - arg.EventCode, - ) + row := q.db.QueryRow(ctx, createEvent, arg.UserUuid, arg.Name, arg.EventCode) var i Event err := row.Scan( &i.UserUuid, @@ -116,6 +110,19 @@ func (q *Queries) GetEvent(ctx context.Context, arg GetEventParams) (Event, erro return i, err } +const getEventUUIDByCode = `-- name: GetEventUUIDByCode :one +Select event_uuid +FROM events +WHERE event_code = $1 +` + +func (q *Queries) GetEventUUIDByCode(ctx context.Context, eventCode string) (uuid.UUID, error) { + row := q.db.QueryRow(ctx, getEventUUIDByCode, eventCode) + var event_uuid uuid.UUID + err := row.Scan(&event_uuid) + return event_uuid, err +} + const getEventUUIDByName = `-- name: GetEventUUIDByName :one SELECT event_uuid FROM events diff --git a/internal/database/migrations/000003_create_playlists_table.up.sql b/internal/database/migrations/000003_create_playlists_table.up.sql index 5d78e9c..8eb6d3f 100644 --- a/internal/database/migrations/000003_create_playlists_table.up.sql +++ b/internal/database/migrations/000003_create_playlists_table.up.sql @@ -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 ); \ No newline at end of file diff --git a/internal/database/playlists.sql.go b/internal/database/playlists.sql.go index 8d0f4ba..893b0d1 100644 --- a/internal/database/playlists.sql.go +++ b/internal/database/playlists.sql.go @@ -107,6 +107,21 @@ func (q *Queries) GetPlaylist(ctx context.Context, arg GetPlaylistParams) (Playl return i, err } +const getPlaylistUUIDByEventUUID = `-- name: GetPlaylistUUIDByEventUUID :one +Select playlist_id +FROM playlists +WHERE event_uuid = $1 +ORDER BY created_at desc +Limit 1 +` + +func (q *Queries) GetPlaylistUUIDByEventUUID(ctx context.Context, eventUuid uuid.UUID) (string, error) { + row := q.db.QueryRow(ctx, getPlaylistUUIDByEventUUID, eventUuid) + var playlist_id string + err := row.Scan(&playlist_id) + return playlist_id, err +} + const getPlaylistUUIDByName = `-- name: GetPlaylistUUIDByName :one SELECT playlist_id FROM playlists diff --git a/internal/database/queries/events.sql b/internal/database/queries/events.sql index d28ed6f..105d79d 100644 --- a/internal/database/queries/events.sql +++ b/internal/database/queries/events.sql @@ -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 @@ -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 diff --git a/internal/database/queries/playlists.sql b/internal/database/queries/playlists.sql index c10a55f..7909281 100644 --- a/internal/database/queries/playlists.sql +++ b/internal/database/queries/playlists.sql @@ -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 diff --git a/internal/server/routes.go b/internal/server/routes.go index 1d349ed..b57051f 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -1,9 +1,15 @@ package server import ( + "database/sql" + "errors" + "fmt" + "math/rand" "net/http" + "spotify-collab/internal/database" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) func (s *Server) RegisterRoutes() http.Handler { @@ -26,3 +32,98 @@ func (s *Server) HelloWorldHandler(c *gin.Context) { func (s *Server) healthHandler(c *gin.Context) { c.JSON(http.StatusOK, s.db.Health()) } + +func (s *Server) AddSongToEvent(c *gin.Context) { + var input struct { + EventCode string `json:"event_code"` + URI string `json:"uri"` + } + + if err := c.ShouldBindJSON(input); err != nil { + c.JSON(http.StatusUnprocessableEntity, gin.H{ + "msg": "invalid format", + }) + } + + q := database.New(s.db.Db) + event, err := q.GetEventUUIDByCode(c, input.EventCode) + if errors.Is(sql.ErrNoRows, err) { + c.JSON(http.StatusNotFound, gin.H{ + "msg": "event not found", + }) + } else if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "msg": err, + }) + } + + playlist, err := q.GetPlaylistUUIDByEventUUID(c, event) + if errors.Is(sql.ErrNoRows, err) { + c.JSON(http.StatusNotFound, gin.H{ + "msg": "no playlist found", + }) + } else if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "msg": err, + }) + } + + // TODO: Check if valid song, passes config -> not greater than count, not blacklisted, other configs + _, err = q.AddSong(c, database.AddSongParams{ + SongUri: input.URI, + PlaylistID: playlist, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "msg": err, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "msg": "song added", + }) +} + +func (s *Server) CreateEvent(c *gin.Context) { + var input struct { + User_uuid uuid.UUID `json:"user_uuid"` + Name string `json:"name"` + } + + if err := c.ShouldBindJSON(input); err != nil { + c.JSON(http.StatusUnprocessableEntity, gin.H{ + "msg": "invalid format", + }) + } + + q := database.New(s.db.Db) + + eventCode := GenerateEventCode(6) + + event, err := q.CreateEvent(c, database.CreateEventParams{ + UserUuid: input.User_uuid, + Name: input.Name, + EventCode: eventCode, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "msg": fmt.Errorf("server error: %w", err).Error(), + }) + } + + c.JSON(http.StatusOK, gin.H{ + "created_at": event.CreatedAt, + "event_uuid": 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) +} From d66bff636d765a9bc3375eecfd6ab66d0a4043c5 Mon Sep 17 00:00:00 2001 From: Aditya-Chowdhary Date: Tue, 9 Jul 2024 22:37:05 +0530 Subject: [PATCH 2/3] feat: base response + server errors --- internal/merrors/conflict_409.go | 25 ++++++++++++ internal/merrors/constants.go | 19 +++++++++ internal/merrors/downstream_550.go | 22 +++++++++++ internal/merrors/forbidden_403.go | 24 ++++++++++++ internal/merrors/handle_service_errors.go | 43 +++++++++++++++++++++ internal/merrors/internal_server_500.go | 27 +++++++++++++ internal/merrors/service_unavailable_503.go | 23 +++++++++++ internal/merrors/unauthorized_401.go | 26 +++++++++++++ internal/merrors/validation_422.go | 27 +++++++++++++ internal/utils/base_response.go | 16 ++++++++ 10 files changed, 252 insertions(+) create mode 100644 internal/merrors/conflict_409.go create mode 100644 internal/merrors/constants.go create mode 100644 internal/merrors/downstream_550.go create mode 100644 internal/merrors/forbidden_403.go create mode 100644 internal/merrors/handle_service_errors.go create mode 100644 internal/merrors/internal_server_500.go create mode 100644 internal/merrors/service_unavailable_503.go create mode 100644 internal/merrors/unauthorized_401.go create mode 100644 internal/merrors/validation_422.go create mode 100644 internal/utils/base_response.go diff --git a/internal/merrors/conflict_409.go b/internal/merrors/conflict_409.go new file mode 100644 index 0000000..3840611 --- /dev/null +++ b/internal/merrors/conflict_409.go @@ -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() +} diff --git a/internal/merrors/constants.go b/internal/merrors/constants.go new file mode 100644 index 0000000..524bc5c --- /dev/null +++ b/internal/merrors/constants.go @@ -0,0 +1,19 @@ +package merrors + +var errorType = struct { + validation string + server string + Unauthorized string + conflict string + ServiceUnavailable string + Forbidden string + Downstream string +}{ + validation: "validation", + server: "server", + Unauthorized: "unauthorized", + conflict: "conflict", + ServiceUnavailable: "service unavailable", + Forbidden: "forbidden", + Downstream: "downstream", +} diff --git a/internal/merrors/downstream_550.go b/internal/merrors/downstream_550.go new file mode 100644 index 0000000..f12aea9 --- /dev/null +++ b/internal/merrors/downstream_550.go @@ -0,0 +1,22 @@ +package merrors + +import ( + "spotify-collab/internal/utils" + + "github.com/gin-gonic/gin" +) + +/* -------------------------------------------------------------------------- */ +/* DOWNSTREAM ERROR */ +/* -------------------------------------------------------------------------- */ +func Downstream(ctx *gin.Context, err string) { + var res utils.BaseResponse + var smerror utils.Error + errorCode := 550 + smerror.Code = errorCode + smerror.Type = errorType.Downstream + smerror.Message = err + res.Error = smerror + ctx.JSON(errorCode, res) + ctx.Abort() +} diff --git a/internal/merrors/forbidden_403.go b/internal/merrors/forbidden_403.go new file mode 100644 index 0000000..c7a6607 --- /dev/null +++ b/internal/merrors/forbidden_403.go @@ -0,0 +1,24 @@ +package merrors + +import ( + "net/http" + + "spotify-collab/internal/utils" + + "github.com/gin-gonic/gin" +) + +/* -------------------------------------------------------------------------- */ +/* FORBIDDEN 403 */ +/* -------------------------------------------------------------------------- */ +func Forbidden(ctx *gin.Context, err string) { + var res utils.BaseResponse + var smerror utils.Error + errorCode := http.StatusForbidden + smerror.Code = errorCode + smerror.Type = errorType.Forbidden + smerror.Message = err + res.Error = smerror + ctx.JSON(errorCode, res) + ctx.Abort() +} diff --git a/internal/merrors/handle_service_errors.go b/internal/merrors/handle_service_errors.go new file mode 100644 index 0000000..048d777 --- /dev/null +++ b/internal/merrors/handle_service_errors.go @@ -0,0 +1,43 @@ +package merrors + +import ( + "net/http" + + "spotify-collab/internal/utils" + + "github.com/gin-gonic/gin" +) + +func HandleServiceCodes(ctx *gin.Context, baseRes utils.BaseResponse) { + switch baseRes.StatusCode { + case http.StatusUnauthorized: + { + Unauthorized(ctx, baseRes.Message) + } + case http.StatusForbidden: + { + Forbidden(ctx, baseRes.Message) + } + case http.StatusServiceUnavailable: + { + ServiceUnavailable(ctx, baseRes.Message) + } + case http.StatusConflict: + { + Conflict(ctx, baseRes.Message) + } + case http.StatusUnprocessableEntity: + { + Validation(ctx, baseRes.Message) + } + case 550: + { + Downstream(ctx, baseRes.Message) + } + + default: + { + InternalServer(ctx, baseRes.Message) + } + } +} diff --git a/internal/merrors/internal_server_500.go b/internal/merrors/internal_server_500.go new file mode 100644 index 0000000..bedd1fa --- /dev/null +++ b/internal/merrors/internal_server_500.go @@ -0,0 +1,27 @@ +package merrors + +import ( + "net/http" + + "spotify-collab/internal/utils" + + "github.com/gin-gonic/gin" +) + +/* -------------------------------------------------------------------------- */ +/* INTERNAL SERVER ERROR 500 */ +/* -------------------------------------------------------------------------- */ +func InternalServer(ctx *gin.Context, err string) { + var res utils.BaseResponse + var smerror utils.Error + errorCode := http.StatusInternalServerError + + smerror.Code = errorCode + smerror.Type = errorType.server + smerror.Message = err + + res.Error = smerror + + ctx.JSON(errorCode, res) + ctx.Abort() +} diff --git a/internal/merrors/service_unavailable_503.go b/internal/merrors/service_unavailable_503.go new file mode 100644 index 0000000..0d7e226 --- /dev/null +++ b/internal/merrors/service_unavailable_503.go @@ -0,0 +1,23 @@ +package merrors + +import ( + "net/http" + + "spotify-collab/internal/utils" + + "github.com/gin-gonic/gin" +) + +func ServiceUnavailable(ctx *gin.Context, err string) { + var res utils.BaseResponse + var smerror utils.Error + errorCode := http.StatusServiceUnavailable + smerror.Code = errorCode + smerror.Type = errorType.ServiceUnavailable + smerror.Message = err + + res.Error = smerror + + ctx.JSON(errorCode, res) + ctx.Abort() +} diff --git a/internal/merrors/unauthorized_401.go b/internal/merrors/unauthorized_401.go new file mode 100644 index 0000000..d49b192 --- /dev/null +++ b/internal/merrors/unauthorized_401.go @@ -0,0 +1,26 @@ +package merrors + +import ( + "net/http" + + "spotify-collab/internal/utils" + + "github.com/gin-gonic/gin" +) + +/* -------------------------------------------------------------------------- */ +/* Unauthorized Error 401 */ +/* -------------------------------------------------------------------------- */ +func Unauthorized(ctx *gin.Context, err string) { + var res utils.BaseResponse + var smerror utils.Error + errorCode := http.StatusUnauthorized + smerror.Code = errorCode + smerror.Type = errorType.Unauthorized + smerror.Message = err + + res.Error = smerror + + ctx.JSON(errorCode, res) + ctx.Abort() +} diff --git a/internal/merrors/validation_422.go b/internal/merrors/validation_422.go new file mode 100644 index 0000000..887c4f9 --- /dev/null +++ b/internal/merrors/validation_422.go @@ -0,0 +1,27 @@ +package merrors + +import ( + "net/http" + + "spotify-collab/internal/utils" + + "github.com/gin-gonic/gin" +) + +/* -------------------------------------------------------------------------- */ +/* VALIDATION ERROR 422 */ +/* -------------------------------------------------------------------------- */ +func Validation(ctx *gin.Context, err string) { + var res utils.BaseResponse + var smerror utils.Error + errorCode := http.StatusUnprocessableEntity + + smerror.Code = errorCode + smerror.Type = errorType.validation + smerror.Message = err + + res.Error = smerror + + ctx.JSON(errorCode, res) + ctx.Abort() +} diff --git a/internal/utils/base_response.go b/internal/utils/base_response.go new file mode 100644 index 0000000..eb4fedf --- /dev/null +++ b/internal/utils/base_response.go @@ -0,0 +1,16 @@ +package utils + +type BaseResponse struct { + Success bool `json:"success,omitempty"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` + MetaData interface{} `json:"metadata,omitempty"` + Error Error `json:"error,omitempty"` + StatusCode int `json:"status_code,omitempty"` +} + +type Error struct { + Code int `json:"code"` + Type string `json:"type"` + Message string `json:"message"` +} From 59d22bcfe49e94a023cff119c5a9d1472af2920c Mon Sep 17 00:00:00 2001 From: Aditya-Chowdhary Date: Wed, 10 Jul 2024 00:29:21 +0530 Subject: [PATCH 3/3] refactor: handler grouping --- internal/controllers/v1/events/handlers.go | 58 ++++++++++++++++++++ internal/controllers/v1/events/models.go | 17 ++++++ internal/controllers/v1/events/validators.go | 9 +++ internal/database/database.go | 15 ++--- internal/server/routes.go | 56 +++---------------- internal/server/server.go | 9 ++- 6 files changed, 104 insertions(+), 60 deletions(-) create mode 100644 internal/controllers/v1/events/handlers.go create mode 100644 internal/controllers/v1/events/models.go create mode 100644 internal/controllers/v1/events/validators.go diff --git a/internal/controllers/v1/events/handlers.go b/internal/controllers/v1/events/handlers.go new file mode 100644 index 0000000..6aa9c95 --- /dev/null +++ b/internal/controllers/v1/events/handlers.go @@ -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) +} diff --git a/internal/controllers/v1/events/models.go b/internal/controllers/v1/events/models.go new file mode 100644 index 0000000..4abf1f6 --- /dev/null +++ b/internal/controllers/v1/events/models.go @@ -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"` +} diff --git a/internal/controllers/v1/events/validators.go b/internal/controllers/v1/events/validators.go new file mode 100644 index 0000000..f36f238 --- /dev/null +++ b/internal/controllers/v1/events/validators.go @@ -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 +} diff --git a/internal/database/database.go b/internal/database/database.go index 9a2eed3..9c38c33 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -34,24 +34,21 @@ 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.Db != nil { - return *dbInstance + 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, - } - return *dbInstance + dbInstance = db + return dbInstance } // Health checks the health of the database connection by pinging the database. diff --git a/internal/server/routes.go b/internal/server/routes.go index b57051f..b18d32a 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -3,13 +3,10 @@ package server import ( "database/sql" "errors" - "fmt" - "math/rand" "net/http" "spotify-collab/internal/database" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) func (s *Server) RegisterRoutes() http.Handler { @@ -19,6 +16,8 @@ func (s *Server) RegisterRoutes() http.Handler { r.GET("/health", s.healthHandler) + r.POST("/events/new", s.eventHandler.CreateEvent) + return r } @@ -30,7 +29,10 @@ func (s *Server) HelloWorldHandler(c *gin.Context) { } func (s *Server) healthHandler(c *gin.Context) { - c.JSON(http.StatusOK, s.db.Health()) + // c.JSON(http.StatusOK, s.db.Health()) + c.JSON(http.StatusOK, gin.H{ + "testing": "ready!", + }) } func (s *Server) AddSongToEvent(c *gin.Context) { @@ -45,7 +47,7 @@ func (s *Server) AddSongToEvent(c *gin.Context) { }) } - q := database.New(s.db.Db) + q := database.New(s.db) event, err := q.GetEventUUIDByCode(c, input.EventCode) if errors.Is(sql.ErrNoRows, err) { c.JSON(http.StatusNotFound, gin.H{ @@ -83,47 +85,3 @@ func (s *Server) AddSongToEvent(c *gin.Context) { "msg": "song added", }) } - -func (s *Server) CreateEvent(c *gin.Context) { - var input struct { - User_uuid uuid.UUID `json:"user_uuid"` - Name string `json:"name"` - } - - if err := c.ShouldBindJSON(input); err != nil { - c.JSON(http.StatusUnprocessableEntity, gin.H{ - "msg": "invalid format", - }) - } - - q := database.New(s.db.Db) - - eventCode := GenerateEventCode(6) - - event, err := q.CreateEvent(c, database.CreateEventParams{ - UserUuid: input.User_uuid, - Name: input.Name, - EventCode: eventCode, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "msg": fmt.Errorf("server error: %w", err).Error(), - }) - } - - c.JSON(http.StatusOK, gin.H{ - "created_at": event.CreatedAt, - "event_uuid": 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) -} diff --git a/internal/server/server.go b/internal/server/server.go index 5dad50e..dd7c80a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,23 +7,28 @@ import ( "strconv" "time" + "github.com/jackc/pgx/v5/pgxpool" _ "github.com/joho/godotenv/autoload" + "spotify-collab/internal/controllers/v1/events" "spotify-collab/internal/database" ) type Server struct { port int - db database.Service + db *pgxpool.Pool + eventHandler *events.EventHandler } func NewServer() *http.Server { port, _ := strconv.Atoi(os.Getenv("PORT")) + db := database.NewService() NewServer := &Server{ port: port, - db: database.NewService(), + db: db, + eventHandler: events.Handler(db), } // Declare Server config