feat: added post methods for strapi service
This commit is contained in:
@@ -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
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user