From 3afb3477ec49f2fa40c1bfc53c9168a1714ca8ce Mon Sep 17 00:00:00 2001 From: darwincereska Date: Tue, 17 Feb 2026 06:23:00 -0500 Subject: [PATCH] feat: added post methods for strapi service --- cmd/main.go | 9 + go.mod | 2 +- go.sum | 4 +- internal/services/strapi.go | 268 +++++++++++++++++++ internal/strapi/queries/post_queries.graphql | 5 +- 5 files changed, 283 insertions(+), 5 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index d7247ab..2d3997f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,8 @@ import ( "blog/internal/database" "blog/internal/config" "github.com/charmbracelet/log" + "blog/internal/cache" + "blog/internal/services" ) func main() { @@ -21,4 +23,11 @@ func main() { log.Fatal("Failed to connect to database: ", err) } defer db.Close() + + // Create Redis caches + strapi_cache := cache.CreateCache(server_config.RedisHost, server_config.RedisPort, 0) + analytics_cache := cache.CreateCache(server_config.RedisHost, server_config.RedisPort, 1) + + // Create Strapi service + strapi_service := services.NewStrapiService(server_config.StrapiHost, server_config.StrapiApiKey, strapi_cache) } diff --git a/go.mod b/go.mod index 6984274..8eb7a88 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/a-h/templ v0.3.977 github.com/charmbracelet/log v0.4.2 github.com/redis/go-redis/v9 v9.17.3 + golang.org/x/sync v0.19.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 ) @@ -43,7 +44,6 @@ require ( golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.26.0 // indirect - golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.35.0 // indirect diff --git a/go.sum b/go.sum index b3ced63..8791ac3 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,8 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.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/services/strapi.go b/internal/services/strapi.go index ec025b1..cbe5e15 100644 --- a/internal/services/strapi.go +++ b/internal/services/strapi.go @@ -1,2 +1,270 @@ package services +import ( + "blog/internal/cache" + repo "blog/internal/repositories" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/Khan/genqlient/graphql" + "golang.org/x/sync/singleflight" +) + +// StrapiService contains all methods for interacting with Strapi +type StrapiService struct { + client graphql.Client + token string // Authorization bearer token + sf *singleflight.Group // Singleflight group to reduce api calls + cache *cache.Cache // Redis cache +} + +func NewStrapiService(endpoint, token string, cache *cache.Cache) *StrapiService { + httpClient := &http.Client{ + Transport: &authTransport{ + token: token, + base: http.DefaultTransport, + }, + } + + return &StrapiService{ + client: graphql.NewClient(endpoint, httpClient), + token: token, + sf: &singleflight.Group{}, + cache: cache, + } +} + +// GetAllPosts returns all posts from Strapi +func (s *StrapiService) GetAllPosts(ctx context.Context, pageSize, page int) ([]repo.Post, error) { + key := fmt.Sprintf("strapi:posts:all:%d:%d", pageSize, page) + + // Check if exists in cache + cached, err := s.cache.Get(ctx, key) + if err == nil { + var posts []repo.Post + if err := json.Unmarshal([]byte(cached), &posts); err == nil { + return posts, nil + } + } + + // Cache miss - use singleflight + result, err, _:= s.sf.Do(key, func() (any, error) { + resp, err := repo.GetAllPosts(ctx, s.client, pageSize, page) + if err != nil { + return nil, err + } + + // Store in cache + go func() { + if data, err := json.Marshal(resp.Posts); err == nil { + // Set cache with 5 minute expiry + s.cache.Set(context.Background(), key, data, time.Minute*5) + } + }() + + return resp.Posts, nil + }) + + if err != nil { + return nil, err + } + + // Return result + posts, ok := result.([]repo.Post) + if !ok { + return nil, fmt.Errorf("unexpected type from singleflight") + } + return posts, nil +} + +// GetFeaturedPosts returns any post that has "featured" as true from Strapi +func (s *StrapiService) GetFeaturedPosts(ctx context.Context, pageSize, page int) ([]repo.PostSummary, error) { + key := fmt.Sprintf("strapi:posts:featured:%d:%d", pageSize, page) + + // Check if exists in cache + cached, err := s.cache.Get(ctx, key) + if err == nil { + var posts []repo.PostSummary + if err := json.Unmarshal([]byte(cached), &posts); err == nil { + return posts, nil + } + } + + // Cache miss - use singleflight + result, err, _ := s.sf.Do(key, func() (any, error) { + resp, err := repo.GetFeaturedPosts(ctx, s.client, pageSize, page) + if err != nil { + return nil, err + } + + // Store in cache + go func() { + if data, err := json.Marshal(resp.Posts); err == nil { + // Set cache with 5 minute expiry + s.cache.Set(context.Background(), key, data, time.Minute*5) + } + }() + + return resp.Posts, nil + }) + + if err != nil { + return nil, err + } + + // Return result + posts, ok := result.([]repo.PostSummary) + if !ok { + return nil, fmt.Errorf("unexpected type from singleflight") + } + return posts, nil +} + +// GetPost returns a specific post from Strapi +func (s *StrapiService) GetPost(ctx context.Context, slug string) (*repo.Post, error) { + key := fmt.Sprintf("strapi:post:%s", slug) + + // Check if exists in cache + cached, err := s.cache.Get(ctx, key) + if err == nil { + var post repo.Post + if err := json.Unmarshal([]byte(cached), &post); err == nil { + return &post, nil + } + } + + // Cache miss - use singleflight + result, err, _ := s.sf.Do(key, func() (any, error) { + resp, err := repo.GetPost(ctx, s.client, slug) + if err != nil { + return nil, err + } + + if len(resp.Posts) == 0 { + return nil, fmt.Errorf("post not found: %s", slug) + } + + post := &resp.Posts[0] // Create pointer + + // Store in cache + go func() { + if data, err := json.Marshal(post); err == nil { + // Set cache with 15 minute expiry + s.cache.Set(context.Background(), key, data, time.Minute*15) + } + }() + + return post, nil + }) + + if err != nil { + return nil, err + } + + // Return result + post, ok := result.(*repo.Post) + if !ok { + return nil, fmt.Errorf("unexpected type from singleflight") + } + return post, nil +} + +// GetPostSummaries returns post summaries from Strapi +func (s *StrapiService) GetPostSummaries(ctx context.Context, pageSize, page int) ([]repo.PostSummary, error) { + key := fmt.Sprintf("strapi:posts:summary:%d:%d", pageSize, page) + + // Check if exists in cache + cached, err := s.cache.Get(ctx, key) + if err == nil { + var posts []repo.PostSummary + if err := json.Unmarshal([]byte(cached), &posts); err == nil { + return posts, nil + } + } + + // Cache miss - use singleflight + result, err, _ := s.sf.Do(key, func() (any, error) { + resp, err := repo.GetPostSummaries(ctx, s.client, pageSize, page) + if err != nil { + return nil, err + } + + // Store in cache + go func() { + if data, err := json.Marshal(resp.Posts); err == nil { + // Set cache with 5 minute expiry + s.cache.Set(context.Background(), key, data, time.Minute*5) + } + }() + + return resp.Posts, nil + }) + + if err != nil { + return nil, err + } + + // Return results + posts, ok := result.([]repo.PostSummary) + if !ok { + return nil, fmt.Errorf("unexpected type from singleflight") + } + return posts, nil +} + +// GetPostsByTag returns posts with a specific tag from Strapi +func (s *StrapiService) GetPostsByTag(ctx context.Context, tag string, pageSize, page int) ([]repo.PostSummary, error) { + key := fmt.Sprintf("strapi:posts:tag:%s:%d:%d", tag, pageSize, page) + + // Check if exists in cache + cached, err := s.cache.Get(ctx, key) + if err == nil { + var posts []repo.PostSummary + if err := json.Unmarshal([]byte(cached), &posts); err == nil { + return posts, nil + } + } + + // Cache miss - use singleflight + result, err, _ := s.sf.Do(key, func() (any, error) { + resp, err := repo.GetPostsByTag(ctx, s.client, tag, pageSize, page) + if err != nil { + return nil, err + } + + // Store in cache + go func() { + if data, err := json.Marshal(resp.Posts); err == nil { + // Store in cache with 5 minute expiry + s.cache.Set(context.Background(), key, data, time.Minute*5) + } + }() + + return resp.Posts, nil + }) + + if err != nil { + return nil, err + } + + // Return results + posts, ok := result.([]repo.PostSummary) + if !ok { + return nil, fmt.Errorf("unexpected type from singleflight") + } + return posts, nil +} + +// Auth transport for headers +type authTransport struct { + token string + base http.RoundTripper +} + +func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Bearer "+t.token) + return t.base.RoundTrip(req) +} diff --git a/internal/strapi/queries/post_queries.graphql b/internal/strapi/queries/post_queries.graphql index dbf13b7..525ef8d 100644 --- a/internal/strapi/queries/post_queries.graphql +++ b/internal/strapi/queries/post_queries.graphql @@ -65,8 +65,9 @@ query GetAllPosts($pageSize: Int = 10, $page: Int = 1) { # Get featured posts query GetFeaturedPosts($pageSize: Int = 10, $page: Int = 1) { # @genqlient(flatten: true) - posts(pagination: { pageSize: $pageSize, page: $page } - filters: { featured: { eq: true } } + posts( + pagination: { pageSize: $pageSize, page: $page } + filters: { featured: { eq: true } } ) { ...PostSummary }