feat: added post methods for strapi service

This commit is contained in:
darwincereska
2026-02-17 06:23:00 -05:00
parent 6384b0d874
commit 3afb3477ec
5 changed files with 283 additions and 5 deletions

View File

@@ -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)
}

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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
}