mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
- 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).
219 lines
5.6 KiB
Go
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()
|
|
}
|