diff --git a/cmd/main.go b/cmd/main.go index 0a0a071..04236f4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,13 +4,13 @@ import ( "blog/internal/cache" "blog/internal/config" "blog/internal/database" + "blog/internal/echo/logger" "blog/internal/echo/routes" "blog/internal/services" "fmt" "github.com/charmbracelet/log" "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" ) func main() { @@ -38,10 +38,12 @@ func main() { // Setup echo server e := echo.New() - e.Use(middleware.RequestLogger()) + e.Logger = logger.NewCharmSlog() // Setup routes - routes.SetupRoutes(e, strapi_service) + routes.SetupRoutes(e, routes.Sources{ + StrapiService: strapi_service, + }) // Start server host := fmt.Sprintf("%s:%d", server_config.Host, server_config.Port) diff --git a/go.mod b/go.mod index c3068f5..d0b1bb0 100644 --- a/go.mod +++ b/go.mod @@ -34,11 +34,15 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/labstack/echo/v5 v5.0.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/lucasb-eyer/go-colorful v1.3.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.20 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vektah/gqlparser/v2 v2.5.31 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/atomic v1.11.0 // indirect diff --git a/go.sum b/go.sum index 6c9042b..d95da23 100644 --- a/go.sum +++ b/go.sum @@ -82,10 +82,15 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc= github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= +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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +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= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -112,6 +117,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vektah/gqlparser/v2 v2.5.19 h1:bhCPCX1D4WWzCDvkPl4+TP1N8/kLrWnp43egplt7iSg= github.com/vektah/gqlparser/v2 v2.5.19/go.mod h1:y7kvl5bBlDeuWIvLtA9849ncyvx6/lj06RsMrEjVy3U= github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= @@ -132,6 +141,7 @@ golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/internal/config/database.go b/internal/config/database.go index cecfe76..60aa536 100644 --- a/internal/config/database.go +++ b/internal/config/database.go @@ -1,18 +1,18 @@ package config import ( - "github.com/charmbracelet/log" "blog/internal/config/env" "fmt" + "github.com/charmbracelet/log" ) type DatabaseConfig struct { - Host string - Port int - Name string - User string + Host string + Port int + Name string + User string Password string - SSLMode string + SSLMode string TimeZone string } @@ -29,7 +29,7 @@ func (c *DatabaseConfig) LoadConfig() { c.SSLMode = env.GetString("DB_SSL_MODE", "disable") c.TimeZone = env.GetString("DB_TIME_ZONE", "America/New_York") - log.Info("Successfully loaded database config") + log.Info("Successfully loaded database config") } func (c *DatabaseConfig) GetDSN() string { diff --git a/internal/config/frontend.go b/internal/config/frontend.go index f3921b0..d912156 100644 --- a/internal/config/frontend.go +++ b/internal/config/frontend.go @@ -1,2 +1 @@ package config - diff --git a/internal/config/server.go b/internal/config/server.go index 0cfe1bf..8c10d92 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -1,19 +1,19 @@ package config import ( - "github.com/charmbracelet/log" "blog/internal/config/env" + "github.com/charmbracelet/log" ) type ServerConfig struct { - Host string - Port int + Host string + Port int StrapiEndpoint string - RedisHost string - RedisPort int - StrapiToken string - CacheTTL int - EchoMode string + RedisHost string + RedisPort int + StrapiToken string + CacheTTL int + EchoMode string } func NewServerConfig() *ServerConfig { diff --git a/internal/database/connection.go b/internal/database/connection.go index b5854a7..a445e5e 100644 --- a/internal/database/connection.go +++ b/internal/database/connection.go @@ -18,14 +18,13 @@ type DB struct { *gorm.DB } - // Connect connects to database and returns DB func Connect(dsn string) (*DB, error) { // Charmbracelet's "log" as a slog handler charmHandler := log.NewWithOptions(os.Stdout, log.Options{ - ReportCaller: false, + ReportCaller: false, ReportTimestamp: true, - Prefix: "GORM", + Prefix: "GORM", }) slogger := slog.New(charmHandler) @@ -34,12 +33,12 @@ func Connect(dsn string) (*DB, error) { gormLogger := logger.New( slog.NewLogLogger(slogger.Handler(), slog.LevelDebug), logger.Config{ - SlowThreshold: time.Millisecond * 200, - LogLevel: logger.Info, + SlowThreshold: time.Millisecond * 200, + LogLevel: logger.Info, IgnoreRecordNotFoundError: false, - Colorful: true, + Colorful: true, }, - ) + ) // Connect to database db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ diff --git a/internal/echo/handlers/params.go b/internal/echo/handlers/params.go new file mode 100644 index 0000000..3ace1e3 --- /dev/null +++ b/internal/echo/handlers/params.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "strconv" + + "github.com/labstack/echo/v5" +) + +// Helper method to get int param or default value +func GetIntParam(c *echo.Context, name string, defaultValue int) int { + value, err := strconv.Atoi(c.ParamOr(name, strconv.Itoa(defaultValue))) + if err != nil { + return defaultValue + } + + return value +} + +// Helper method to get bool param or default value +func GetBoolParam(c *echo.Context, name string, defaultValue bool) bool { + value, err := strconv.ParseBool(c.ParamOr(name, strconv.FormatBool(defaultValue))) + if err != nil { + return defaultValue + } + + return value +} diff --git a/internal/echo/handlers/post_handler.go b/internal/echo/handlers/post_handler.go index 6122bea..7f1de0d 100644 --- a/internal/echo/handlers/post_handler.go +++ b/internal/echo/handlers/post_handler.go @@ -8,7 +8,10 @@ import ( ) // GetAllPosts returns a list of all posts -func GetAllPosts(c *echo.Context, s *services.StrapiService, pageSize, page int) error { +func GetAllPosts(c *echo.Context, s *services.StrapiService) error { + pageSize := GetIntParam(c, "pageSize", 10) + page := GetIntParam(c, "page", 1) + posts, err := s.GetAllPosts(c.Request().Context(), pageSize, page) if err != nil { return c.JSON(http.StatusInternalServerError, err) @@ -18,7 +21,10 @@ func GetAllPosts(c *echo.Context, s *services.StrapiService, pageSize, page int) } // GetFeaturedPosts returns a list of featured posts -func GetFeaturedPosts(c *echo.Context, s *services.StrapiService, pageSize, page int) error { +func GetFeaturedPosts(c *echo.Context, s *services.StrapiService) error { + pageSize := GetIntParam(c, "pageSize", 10) + page := GetIntParam(c, "page", 1) + posts, err := s.GetFeaturedPosts(c.Request().Context(), pageSize, page) if err != nil { return c.JSON(http.StatusInternalServerError, err) @@ -38,7 +44,10 @@ func GetPost(c *echo.Context, s *services.StrapiService, slug string) error { } // GetPostSummaries returns post summaries -func GetPostSummaries(c *echo.Context, s *services.StrapiService, pageSize, page int) error { +func GetPostSummaries(c *echo.Context, s *services.StrapiService) error { + pageSize := GetIntParam(c, "pageSize", 10) + page := GetIntParam(c, "page", 1) + posts, err := s.GetPostSummaries(c.Request().Context(), pageSize, page) if err != nil { return c.JSON(http.StatusInternalServerError, err) diff --git a/internal/echo/logger/logger.go b/internal/echo/logger/logger.go new file mode 100644 index 0000000..1d56383 --- /dev/null +++ b/internal/echo/logger/logger.go @@ -0,0 +1,23 @@ +package logger + +import ( + "log/slog" + "os" + + "github.com/charmbracelet/log" +) + +// NewCharmSlog returns a standard *slog.Logger powered by Charmbracelet +func NewCharmSlog() *slog.Logger { + // 1. Initialize Charmbracelet + options := log.Options{ + ReportTimestamp: true, + ReportCaller: true, + Level: log.DebugLevel, + } + handler := log.NewWithOptions(os.Stderr, options) + + // 2. Return as *slog.Logger + // Charmbracelet's Logger implements the slog.Handler interface + return slog.New(handler) +} diff --git a/internal/echo/middleware/middleware.go b/internal/echo/middleware/middleware.go index 4dbfda0..5129d75 100644 --- a/internal/echo/middleware/middleware.go +++ b/internal/echo/middleware/middleware.go @@ -1,15 +1,15 @@ package middleware import ( - "time" + // "time" - "github.com/charmbracelet/log" + // "github.com/charmbracelet/log" "github.com/labstack/echo/v5" ) func ServerHandler(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { - start := time.Now() + // start := time.Now() // Process the request err := next(c) @@ -17,16 +17,16 @@ func ServerHandler(next echo.HandlerFunc) echo.HandlerFunc { c.Logger().Error(err.Error()) } - stop := time.Now() - req := c.Request() + // stop := time.Now() + // req := c.Request() - // Log using charmbracelet - log.Info("Request handled", - "method", req.Method, - "path", req.URL.Path, - "latency", stop.Sub(start), - "ip", c.RealIP(), - ) + // // Log using charmbracelet + // log.Info("Request handlers", + // "method", req.Method, + // "path", req.URL.Path, + // "latency", stop.Sub(start), + // "ip", c.RealIP(), + // ) return nil } diff --git a/internal/echo/routes/routes.go b/internal/echo/routes/routes.go index ecc023b..b4a1f03 100644 --- a/internal/echo/routes/routes.go +++ b/internal/echo/routes/routes.go @@ -1,36 +1,53 @@ package routes import ( + "blog/internal/cache" "blog/internal/echo/handlers" "blog/internal/echo/middleware" "blog/internal/services" - "strconv" + "net/http" + "github.com/charmbracelet/log" "github.com/labstack/echo/v5" ) -// Helper method to get int param or default value -func getIntParam(c *echo.Context, name string, defaultValue int) int { - value, err := strconv.Atoi(c.ParamOr(name, strconv.Itoa(defaultValue))) - if err != nil { - return defaultValue - } - - return value +// Souces is a struct that has the sources for any data that the routes need +type Sources struct { + StrapiService *services.StrapiService + Caches []cache.Cache } -func SetupRoutes(e *echo.Echo, s *services.StrapiService) { +func SetupRoutes(e *echo.Echo, sources Sources) { + if sources.StrapiService == nil { + log.Fatal("Error", "error", "strapi service is required") + } + // Global middleware e.Use(middleware.ServerHandler) - // Post routes - posts := e.Group("/posts") // Routing group + // Routes + strapiRoutes(e, sources.StrapiService) - // GET /posts/all - posts.GET("/all", func(c *echo.Context) error { - pageSize := getIntParam(c, "pageSize", 10) - page := getIntParam(c, "page", 1) - - return handlers.GetAllPosts(c, s, pageSize, page) + // Special routes + e.GET("/api", func(c *echo.Context) error { + // Load all routes + // routes, _ := json.MarshalIndent(e.Router().Routes(), "", "") + return c.JSON(http.StatusOK, e.Router().Routes()) + }) +} + +// Setup Strapi routes +func strapiRoutes(e *echo.Echo, s *services.StrapiService) { + // Post routes + posts := e.Group("/api/posts") // Routing group + + // GET /api/posts/all + posts.GET("/all", func(c *echo.Context) error { + return handlers.GetAllPosts(c, s) + }) + + // GET /api/posts/featured + posts.GET("/featured", func(c *echo.Context) error { + return handlers.GetFeaturedPosts(c, s) }) } diff --git a/internal/email/service.go b/internal/email/service.go index 1dfc537..8774a6e 100644 --- a/internal/email/service.go +++ b/internal/email/service.go @@ -1,2 +1 @@ package email - diff --git a/internal/markdown/parser.go b/internal/markdown/parser.go index 8a0943b..b868feb 100644 --- a/internal/markdown/parser.go +++ b/internal/markdown/parser.go @@ -1,2 +1 @@ package markdown - diff --git a/internal/models/api.go b/internal/models/api.go index 276f09d..2640e7f 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -1,3 +1 @@ package models - - diff --git a/internal/models/blog.go b/internal/models/blog.go index 3db3fc6..ed2c6cd 100644 --- a/internal/models/blog.go +++ b/internal/models/blog.go @@ -3,77 +3,77 @@ package models import "time" type Post struct { - ID uint `json:"id"` - Title string `json:"title"` - Slug string `json:"slug"` - Content string `json:"content"` - Excerpt string `json:"excerpt"` - FeaturedImage *Media `json:"featured_image"` - Published bool `json:"published"` - PublishedAt *time.Time `json:"published_at"` - MetaTitle string `json:"meta_title"` - MetaDesc string `json:"meta_description"` - ReadingTime int `json:"reading_time"` - Author Author `json:"author"` - Tags []Tag `json:"tags"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `json:"id"` + Title string `json:"title"` + Slug string `json:"slug"` + Content string `json:"content"` + Excerpt string `json:"excerpt"` + FeaturedImage *Media `json:"featured_image"` + Published bool `json:"published"` + PublishedAt *time.Time `json:"published_at"` + MetaTitle string `json:"meta_title"` + MetaDesc string `json:"meta_description"` + ReadingTime int `json:"reading_time"` + Author Author `json:"author"` + Tags []Tag `json:"tags"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type Tag struct { - ID uint `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Color string `json:"color,omitempty"` + ID uint `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Color string `json:"color,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Author struct { - ID uint `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - Bio string `json:"bio"` - Avatar *Media `json:"avatar"` + ID uint `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Bio string `json:"bio"` + Avatar *Media `json:"avatar"` SocialLinks map[string]string `json:"social_links"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type Media struct { - ID uint `json:"id"` - Name string `json:"name"` - AlternativeText string `json:"alternativeText"` - Caption string `json:"caption"` - Width int `json:"width"` - Height int `json:"height"` - Formats MediaFormats `json:"formats"` - Hash string `json:"hash"` - Ext string `json:"ext"` - Mime string `json:"mime"` - Size float64 `json:"size"` - URL string `json:"url"` - PreviewURL string `json:"previewUrl"` - Provider string `json:"provider"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID uint `json:"id"` + Name string `json:"name"` + AlternativeText string `json:"alternativeText"` + Caption string `json:"caption"` + Width int `json:"width"` + Height int `json:"height"` + Formats MediaFormats `json:"formats"` + Hash string `json:"hash"` + Ext string `json:"ext"` + Mime string `json:"mime"` + Size float64 `json:"size"` + URL string `json:"url"` + PreviewURL string `json:"previewUrl"` + Provider string `json:"provider"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type MediaFormats struct { - Large *MediaFormat `json:"large,omitempty"` - Medium *MediaFormat `json:"medium,omitempty"` - Small *MediaFormat `json:"small,omitempty"` - Thumbnail *MediaFormat `json:"thumbnail,omitempty"` + Large *MediaFormat `json:"large,omitempty"` + Medium *MediaFormat `json:"medium,omitempty"` + Small *MediaFormat `json:"small,omitempty"` + Thumbnail *MediaFormat `json:"thumbnail,omitempty"` } type MediaFormat struct { - Name string `json:"name"` - Hash string `json:"hash"` - Ext string `json:"ext"` - Mime string `json:"mime"` - Width int `json:"width"` - Height int `json:"height"` - Size float64 `json:"size"` - Path string `json:"path"` - URL string `json:"url"` + Name string `json:"name"` + Hash string `json:"hash"` + Ext string `json:"ext"` + Mime string `json:"mime"` + Width int `json:"width"` + Height int `json:"height"` + Size float64 `json:"size"` + Path string `json:"path"` + URL string `json:"url"` } diff --git a/internal/repositories/analytics.go b/internal/repositories/analytics.go index 2a952e3..3f43206 100644 --- a/internal/repositories/analytics.go +++ b/internal/repositories/analytics.go @@ -1,2 +1 @@ package repositories - diff --git a/internal/repositories/newsletter.go b/internal/repositories/newsletter.go index 2a952e3..3f43206 100644 --- a/internal/repositories/newsletter.go +++ b/internal/repositories/newsletter.go @@ -1,2 +1 @@ package repositories - diff --git a/internal/rss/rss.go b/internal/rss/rss.go index 3b593b4..bec542e 100644 --- a/internal/rss/rss.go +++ b/internal/rss/rss.go @@ -3,4 +3,3 @@ package rss import ( "encoding/xml" ) - diff --git a/internal/services/analytics.go b/internal/services/analytics.go index ec025b1..5e568ea 100644 --- a/internal/services/analytics.go +++ b/internal/services/analytics.go @@ -1,2 +1 @@ package services - diff --git a/internal/services/newsletter.go b/internal/services/newsletter.go index ec025b1..5e568ea 100644 --- a/internal/services/newsletter.go +++ b/internal/services/newsletter.go @@ -1,2 +1 @@ package services -