diff --git a/api/go.mod b/api/go.mod index 9894338..3032940 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,6 +4,7 @@ go 1.23.3 require ( github.com/MicahParks/keyfunc/v3 v3.3.5 + github.com/go-playground/validator/v10 v10.23.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/jackc/pgx/v5 v5.7.2 github.com/labstack/echo/v4 v4.13.3 @@ -12,9 +13,13 @@ require ( require ( github.com/MicahParks/jwkset v0.5.19 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/api/go.sum b/api/go.sum index fde0abe..81deb0b 100644 --- a/api/go.sum +++ b/api/go.sum @@ -5,6 +5,16 @@ github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74M github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -19,6 +29,8 @@ github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaa github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= diff --git a/api/internal/handler/conn.go b/api/internal/handler/conn.go index 80d1852..b97145e 100644 --- a/api/internal/handler/conn.go +++ b/api/internal/handler/conn.go @@ -19,7 +19,7 @@ func (h *Handler) AcquireConnection(ctx context.Context, c echo.Context) (*UserI return nil, err } - conn, err := h.queries.AcquireConnection(ctx, h.pool) + conn, err := h.queries.AcquireConnection(ctx, h.pool, userID) if err != nil { return nil, err } diff --git a/api/internal/handler/todo.go b/api/internal/handler/todo.go index 5adaf78..d448caf 100644 --- a/api/internal/handler/todo.go +++ b/api/internal/handler/todo.go @@ -1,9 +1,12 @@ package handler import ( + "log" "net/http" + "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" + "github.com/ryichk/todolist/api/internal/model" ) func (h *Handler) ListTodos(c echo.Context) error { @@ -22,3 +25,45 @@ func (h *Handler) ListTodos(c echo.Context) error { return c.JSON(http.StatusOK, todos) } + +type CreateTodoRequestBody struct { + Title string `json:"title" validate:"required,min=1"` + Note pgtype.Text `json:"note"` +} + +func (h *Handler) CreateTodo(c echo.Context) error { + ctx := c.Request().Context() + + var body CreateTodoRequestBody + if err := c.Bind(&body); err != nil { + log.Printf("invalid request payload: %v", err) + return echo.NewHTTPError(http.StatusBadRequest, "Invalid request payload") + } + if err := c.Validate(body); err != nil { + log.Printf("validation failed: %v", err) + return echo.NewHTTPError(http.StatusBadRequest, "Validation failed") + } + + userInfo, err := h.AcquireConnection(ctx, c) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + defer userInfo.Conn.Release() + + createTodoParams := model.NewCreateTodoParams( + userInfo.UserID, + body.Title, + body.Note, + ) + if todoID, err := h.queries.CreateTodo(ctx, userInfo.Conn, *createTodoParams); err != nil { + log.Printf("failed to create todo: %v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create todo") + } else { + log.Printf("Todo ID: %v", todoID) + } + + response := map[string]string{ + "message": "Successfully created a todo", + } + return c.JSON(http.StatusCreated, response) +} diff --git a/api/internal/model/conn.go b/api/internal/model/conn.go index 3e85ae8..1d19962 100644 --- a/api/internal/model/conn.go +++ b/api/internal/model/conn.go @@ -3,14 +3,20 @@ package model import ( "context" + "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" ) -func (q *Queries) AcquireConnection(ctx context.Context, pool *pgxpool.Pool) (*pgxpool.Conn, error) { +func (q *Queries) AcquireConnection(ctx context.Context, pool *pgxpool.Pool, userID pgtype.UUID) (*pgxpool.Conn, error) { conn, err := pool.Acquire(ctx) if err != nil { return nil, err } + if _, err := conn.Exec(ctx, "SELECT set_config('app.current_user_id', $1, false)", userID); err != nil { + conn.Release() + return nil, err + } + return conn, nil } diff --git a/api/internal/model/todo.go b/api/internal/model/todo.go new file mode 100644 index 0000000..dc81d5e --- /dev/null +++ b/api/internal/model/todo.go @@ -0,0 +1,11 @@ +package model + +import "github.com/jackc/pgx/v5/pgtype" + +func NewCreateTodoParams(userID pgtype.UUID, title string, note pgtype.Text) *CreateTodoParams { + return &CreateTodoParams{ + UserID: userID, + Title: title, + Note: note, + } +} diff --git a/api/internal/model/validator.go b/api/internal/model/validator.go new file mode 100644 index 0000000..67a2f88 --- /dev/null +++ b/api/internal/model/validator.go @@ -0,0 +1,14 @@ +package model + +import "github.com/go-playground/validator/v10" + +type CustomValidator struct { + Validator *validator.Validate +} + +func (cv *CustomValidator) Validate(i interface{}) error { + if err := cv.Validator.Struct(i); err != nil { + return err + } + return nil +} diff --git a/api/internal/server/router.go b/api/internal/server/router.go index f326591..a8018b2 100644 --- a/api/internal/server/router.go +++ b/api/internal/server/router.go @@ -16,4 +16,5 @@ func PrivateRoutes(e *echo.Echo, h *handler.Handler, db *pgxpool.Pool, queries * e.Use(AuthMiddleware()) e.GET("/todos", h.ListTodos) + e.POST("/todos", h.CreateTodo) } diff --git a/api/internal/server/server.go b/api/internal/server/server.go index db975e8..8b65d92 100644 --- a/api/internal/server/server.go +++ b/api/internal/server/server.go @@ -4,6 +4,7 @@ import ( "os" "time" + "github.com/go-playground/validator/v10" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -27,6 +28,8 @@ func NewServer(db *pgxpool.Pool) (*echo.Echo, error) { e.Server.ReadHeaderTimeout = 10 * time.Second e.Server.WriteTimeout = 10 * time.Second + e.Validator = &model.CustomValidator{Validator: validator.New(validator.WithRequiredStructEnabled())} + queries := model.New() h := handler.NewHandler(db, queries)