feat: added post methods for strapi service
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
|||||||
"blog/internal/database"
|
"blog/internal/database"
|
||||||
"blog/internal/config"
|
"blog/internal/config"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
"blog/internal/cache"
|
||||||
|
"blog/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -21,4 +23,11 @@ func main() {
|
|||||||
log.Fatal("Failed to connect to database: ", err)
|
log.Fatal("Failed to connect to database: ", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
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/a-h/templ v0.3.977
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/redis/go-redis/v9 v9.17.3
|
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/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
@@ -43,7 +44,6 @@ require (
|
|||||||
golang.org/x/crypto v0.31.0 // indirect
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
golang.org/x/mod v0.26.0 // 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/sys v0.34.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
golang.org/x/tools v0.35.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/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 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
|||||||
@@ -1,2 +1,270 @@
|
|||||||
package services
|
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,7 +65,8 @@ query GetAllPosts($pageSize: Int = 10, $page: Int = 1) {
|
|||||||
# Get featured posts
|
# Get featured posts
|
||||||
query GetFeaturedPosts($pageSize: Int = 10, $page: Int = 1) {
|
query GetFeaturedPosts($pageSize: Int = 10, $page: Int = 1) {
|
||||||
# @genqlient(flatten: true)
|
# @genqlient(flatten: true)
|
||||||
posts(pagination: { pageSize: $pageSize, page: $page }
|
posts(
|
||||||
|
pagination: { pageSize: $pageSize, page: $page }
|
||||||
filters: { featured: { eq: true } }
|
filters: { featured: { eq: true } }
|
||||||
) {
|
) {
|
||||||
...PostSummary
|
...PostSummary
|
||||||
|
|||||||
Reference in New Issue
Block a user