tercul-backend/internal/platform/cache/redis_cache.go
google-labs-jules[bot] 6d40b4c686 feat: add security middleware, graphql apq, and improved linting
- Add RateLimit, RequestValidation, and CORS middleware.
- Configure middleware chain in API server.
- Implement Redis cache for GraphQL Automatic Persisted Queries.
- Add .golangci.yml and fix linting issues (shadowing, timeouts).
2025-11-30 21:17:43 +00:00

219 lines
5.6 KiB
Go

package cache
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"tercul/internal/platform/config"
"tercul/internal/platform/log"
"github.com/redis/go-redis/v9"
)
// RedisCache implements the Cache interface using Redis
type RedisCache struct {
client *redis.Client
keyGenerator KeyGenerator
defaultExpiry time.Duration
}
// NewRedisCache creates a new RedisCache
func NewRedisCache(client *redis.Client, keyGenerator KeyGenerator, defaultExpiry time.Duration) *RedisCache {
if keyGenerator == nil {
keyGenerator = NewDefaultKeyGenerator("")
}
if defaultExpiry == 0 {
defaultExpiry = 1 * time.Hour // Default expiry of 1 hour
}
return &RedisCache{
client: client,
keyGenerator: keyGenerator,
defaultExpiry: defaultExpiry,
}
}
// NewDefaultRedisCache creates a new RedisCache with default settings
func NewDefaultRedisCache(cfg *config.Config) (*RedisCache, error) {
// Create Redis client from config
client := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr,
Password: cfg.RedisPassword,
DB: cfg.RedisDB,
})
// Test connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
return nil, err
}
return NewRedisCache(client, nil, 0), nil
}
// Get retrieves a value from the cache
func (c *RedisCache) Get(ctx context.Context, key string, value interface{}) error {
data, err := c.client.Get(ctx, key).Bytes()
if err != nil {
if err == redis.Nil {
return errors.New("cache miss")
}
return err
}
return json.Unmarshal(data, value)
}
// Set stores a value in the cache with an optional expiration
func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
if expiration == 0 {
expiration = c.defaultExpiry
}
return c.client.Set(ctx, key, data, expiration).Err()
}
// Delete removes a value from the cache
func (c *RedisCache) Delete(ctx context.Context, key string) error {
return c.client.Del(ctx, key).Err()
}
// Clear removes all values from the cache
func (c *RedisCache) Clear(ctx context.Context) error {
return c.client.FlushAll(ctx).Err()
}
// GetMulti retrieves multiple values from the cache
func (c *RedisCache) GetMulti(ctx context.Context, keys []string) (map[string][]byte, error) {
if len(keys) == 0 {
return make(map[string][]byte), nil
}
values, err := c.client.MGet(ctx, keys...).Result()
if err != nil {
return nil, err
}
result := make(map[string][]byte, len(keys))
for i, key := range keys {
if values[i] == nil {
continue
}
str, ok := values[i].(string)
if !ok {
log.FromContext(ctx).With("key", key).With("type", fmt.Sprintf("%T", values[i])).Warn("Invalid type in Redis cache")
continue
}
result[key] = []byte(str)
}
return result, nil
}
// SetMulti stores multiple values in the cache with an optional expiration
func (c *RedisCache) SetMulti(ctx context.Context, items map[string]interface{}, expiration time.Duration) error {
if len(items) == 0 {
return nil
}
if expiration == 0 {
expiration = c.defaultExpiry
}
pipe := c.client.Pipeline()
for key, value := range items {
data, err := json.Marshal(value)
if err != nil {
return err
}
pipe.Set(ctx, key, data, expiration)
}
_, err := pipe.Exec(ctx)
return err
}
// GetEntity retrieves an entity by ID from the cache
func (c *RedisCache) GetEntity(ctx context.Context, entityType string, id uint, value interface{}) error {
key := c.keyGenerator.EntityKey(entityType, id)
return c.Get(ctx, key, value)
}
// SetEntity stores an entity in the cache
func (c *RedisCache) SetEntity(ctx context.Context, entityType string, id uint, value interface{}, expiration time.Duration) error {
key := c.keyGenerator.EntityKey(entityType, id)
return c.Set(ctx, key, value, expiration)
}
// DeleteEntity removes an entity from the cache
func (c *RedisCache) DeleteEntity(ctx context.Context, entityType string, id uint) error {
key := c.keyGenerator.EntityKey(entityType, id)
return c.Delete(ctx, key)
}
// GetList retrieves a list of entities from the cache
func (c *RedisCache) GetList(ctx context.Context, entityType string, page, pageSize int, value interface{}) error {
key := c.keyGenerator.ListKey(entityType, page, pageSize)
return c.Get(ctx, key, value)
}
// SetList stores a list of entities in the cache
func (c *RedisCache) SetList(
ctx context.Context,
entityType string,
page, pageSize int,
value interface{},
expiration time.Duration,
) error {
key := c.keyGenerator.ListKey(entityType, page, pageSize)
return c.Set(ctx, key, value, expiration)
}
// DeleteList removes a list of entities from the cache
func (c *RedisCache) DeleteList(ctx context.Context, entityType string, page, pageSize int) error {
key := c.keyGenerator.ListKey(entityType, page, pageSize)
return c.Delete(ctx, key)
}
// InvalidateEntityType removes all cached data for a specific entity type
func (c *RedisCache) InvalidateEntityType(ctx context.Context, entityType string) error {
pattern := c.keyGenerator.(*DefaultKeyGenerator).Prefix + entityType + ":*"
// Use SCAN to find all keys matching the pattern
iter := c.client.Scan(ctx, 0, pattern, 100).Iterator()
var keys []string
for iter.Next(ctx) {
keys = append(keys, iter.Val())
// Delete in batches of 100 to avoid large operations
if len(keys) >= 100 {
if err := c.client.Del(ctx, keys...).Err(); err != nil {
return err
}
keys = keys[:0]
}
}
// Delete any remaining keys
if len(keys) > 0 {
return c.client.Del(ctx, keys...).Err()
}
return iter.Err()
}