Add Redis caching for lookup data and admin cache management
- Add lookup-specific cache keys and methods to CacheService - Add cache refresh on lookup CRUD operations in AdminLookupHandler - Add Redis caching after seed-lookups in AdminSettingsHandler - Add ETag generation for seeded data to support client-side caching - Update task template handler with cache invalidation - Fix route for clear-cache endpoint 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
// AdminLookupHandler handles admin lookup table management endpoints
|
||||
@@ -21,6 +24,159 @@ func NewAdminLookupHandler(db *gorm.DB) *AdminLookupHandler {
|
||||
return &AdminLookupHandler{db: db}
|
||||
}
|
||||
|
||||
// refreshCategoriesCache invalidates and refreshes the categories cache
|
||||
func (h *AdminLookupHandler) refreshCategoriesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var categories []models.TaskCategory
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&categories).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch categories for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheCategories(ctx, categories); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache categories")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(categories)).Msg("Refreshed categories cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshPrioritiesCache invalidates and refreshes the priorities cache
|
||||
func (h *AdminLookupHandler) refreshPrioritiesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var priorities []models.TaskPriority
|
||||
if err := h.db.Order("display_order ASC, level ASC").Find(&priorities).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch priorities for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CachePriorities(ctx, priorities); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache priorities")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(priorities)).Msg("Refreshed priorities cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshStatusesCache invalidates and refreshes the statuses cache
|
||||
func (h *AdminLookupHandler) refreshStatusesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var statuses []models.TaskStatus
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch statuses for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheStatuses(ctx, statuses); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache statuses")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(statuses)).Msg("Refreshed statuses cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshFrequenciesCache invalidates and refreshes the frequencies cache
|
||||
func (h *AdminLookupHandler) refreshFrequenciesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var frequencies []models.TaskFrequency
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&frequencies).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch frequencies for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheFrequencies(ctx, frequencies); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache frequencies")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(frequencies)).Msg("Refreshed frequencies cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshResidenceTypesCache invalidates and refreshes the residence types cache
|
||||
func (h *AdminLookupHandler) refreshResidenceTypesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var types []models.ResidenceType
|
||||
if err := h.db.Order("name ASC").Find(&types).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch residence types for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheResidenceTypes(ctx, types); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache residence types")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(types)).Msg("Refreshed residence types cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// refreshSpecialtiesCache invalidates and refreshes the specialties cache
|
||||
func (h *AdminLookupHandler) refreshSpecialtiesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var specialties []models.ContractorSpecialty
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&specialties).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch specialties for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheSpecialties(ctx, specialties); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache specialties")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(specialties)).Msg("Refreshed specialties cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
h.invalidateSeededDataCache(ctx)
|
||||
}
|
||||
|
||||
// invalidateSeededDataCache invalidates the unified seeded data cache
|
||||
// This should be called whenever any lookup data changes
|
||||
func (h *AdminLookupHandler) invalidateSeededDataCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.InvalidateSeededData(ctx); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to invalidate seeded data cache")
|
||||
return
|
||||
}
|
||||
log.Debug().Msg("Invalidated seeded data cache")
|
||||
}
|
||||
|
||||
// ========== Task Categories ==========
|
||||
|
||||
type TaskCategoryResponse struct {
|
||||
@@ -84,6 +240,9 @@ func (h *AdminLookupHandler) CreateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshCategoriesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskCategoryResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
@@ -130,6 +289,9 @@ func (h *AdminLookupHandler) UpdateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshCategoriesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskCategoryResponse{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
@@ -160,6 +322,9 @@ func (h *AdminLookupHandler) DeleteCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshCategoriesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -222,6 +387,9 @@ func (h *AdminLookupHandler) CreatePriority(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshPrioritiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskPriorityResponse{
|
||||
ID: priority.ID,
|
||||
Name: priority.Name,
|
||||
@@ -266,6 +434,9 @@ func (h *AdminLookupHandler) UpdatePriority(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshPrioritiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskPriorityResponse{
|
||||
ID: priority.ID,
|
||||
Name: priority.Name,
|
||||
@@ -294,6 +465,9 @@ func (h *AdminLookupHandler) DeletePriority(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshPrioritiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Priority deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -356,6 +530,9 @@ func (h *AdminLookupHandler) CreateStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshStatusesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskStatusResponse{
|
||||
ID: status.ID,
|
||||
Name: status.Name,
|
||||
@@ -400,6 +577,9 @@ func (h *AdminLookupHandler) UpdateStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshStatusesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskStatusResponse{
|
||||
ID: status.ID,
|
||||
Name: status.Name,
|
||||
@@ -428,6 +608,9 @@ func (h *AdminLookupHandler) DeleteStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshStatusesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Status deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -486,6 +669,9 @@ func (h *AdminLookupHandler) CreateFrequency(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshFrequenciesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, TaskFrequencyResponse{
|
||||
ID: frequency.ID,
|
||||
Name: frequency.Name,
|
||||
@@ -528,6 +714,9 @@ func (h *AdminLookupHandler) UpdateFrequency(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshFrequenciesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, TaskFrequencyResponse{
|
||||
ID: frequency.ID,
|
||||
Name: frequency.Name,
|
||||
@@ -555,6 +744,9 @@ func (h *AdminLookupHandler) DeleteFrequency(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshFrequenciesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Frequency deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -600,6 +792,9 @@ func (h *AdminLookupHandler) CreateResidenceType(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshResidenceTypesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, ResidenceTypeResponse{
|
||||
ID: residenceType.ID,
|
||||
Name: residenceType.Name,
|
||||
@@ -635,6 +830,9 @@ func (h *AdminLookupHandler) UpdateResidenceType(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshResidenceTypesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, ResidenceTypeResponse{
|
||||
ID: residenceType.ID,
|
||||
Name: residenceType.Name,
|
||||
@@ -660,6 +858,9 @@ func (h *AdminLookupHandler) DeleteResidenceType(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshResidenceTypesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Residence type deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -722,6 +923,9 @@ func (h *AdminLookupHandler) CreateSpecialty(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshSpecialtiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, ContractorSpecialtyResponse{
|
||||
ID: specialty.ID,
|
||||
Name: specialty.Name,
|
||||
@@ -766,6 +970,9 @@ func (h *AdminLookupHandler) UpdateSpecialty(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshSpecialtiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, ContractorSpecialtyResponse{
|
||||
ID: specialty.ID,
|
||||
Name: specialty.Name,
|
||||
@@ -795,6 +1002,9 @@ func (h *AdminLookupHandler) DeleteSpecialty(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshSpecialtiesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Specialty deleted successfully"})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -8,9 +9,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
// AdminSettingsHandler handles system settings management
|
||||
@@ -85,7 +88,7 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// SeedLookups handles POST /api/admin/settings/seed-lookups
|
||||
// Seeds both lookup tables AND task templates
|
||||
// Seeds both lookup tables AND task templates, then caches all lookups in Redis
|
||||
func (h *AdminSettingsHandler) SeedLookups(c *gin.Context) {
|
||||
// First seed lookup tables
|
||||
if err := h.runSeedFile("001_lookups.sql"); err != nil {
|
||||
@@ -99,7 +102,230 @@ func (h *AdminSettingsHandler) SeedLookups(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Lookup data and task templates seeded successfully"})
|
||||
// Cache all lookups in Redis
|
||||
cached, cacheErr := h.cacheAllLookups(c.Request.Context())
|
||||
if cacheErr != nil {
|
||||
log.Warn().Err(cacheErr).Msg("Failed to cache lookups in Redis, but seed was successful")
|
||||
}
|
||||
|
||||
response := gin.H{
|
||||
"message": "Lookup data and task templates seeded successfully",
|
||||
"redis_cached": cached,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// cacheAllLookups fetches all lookup data from the database and caches it in Redis
|
||||
func (h *AdminSettingsHandler) cacheAllLookups(ctx context.Context) (bool, error) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return false, fmt.Errorf("Redis cache not available")
|
||||
}
|
||||
|
||||
// Fetch and cache task categories
|
||||
var categories []models.TaskCategory
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&categories).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch categories: %w", err)
|
||||
}
|
||||
if err := cache.CacheCategories(ctx, categories); err != nil {
|
||||
return false, fmt.Errorf("failed to cache categories: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(categories)).Msg("Cached task categories")
|
||||
|
||||
// Fetch and cache task priorities
|
||||
var priorities []models.TaskPriority
|
||||
if err := h.db.Order("display_order ASC, level ASC").Find(&priorities).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch priorities: %w", err)
|
||||
}
|
||||
if err := cache.CachePriorities(ctx, priorities); err != nil {
|
||||
return false, fmt.Errorf("failed to cache priorities: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(priorities)).Msg("Cached task priorities")
|
||||
|
||||
// Fetch and cache task statuses
|
||||
var statuses []models.TaskStatus
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch statuses: %w", err)
|
||||
}
|
||||
if err := cache.CacheStatuses(ctx, statuses); err != nil {
|
||||
return false, fmt.Errorf("failed to cache statuses: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(statuses)).Msg("Cached task statuses")
|
||||
|
||||
// Fetch and cache task frequencies
|
||||
var frequencies []models.TaskFrequency
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&frequencies).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch frequencies: %w", err)
|
||||
}
|
||||
if err := cache.CacheFrequencies(ctx, frequencies); err != nil {
|
||||
return false, fmt.Errorf("failed to cache frequencies: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(frequencies)).Msg("Cached task frequencies")
|
||||
|
||||
// Fetch and cache residence types
|
||||
var residenceTypes []models.ResidenceType
|
||||
if err := h.db.Order("name ASC").Find(&residenceTypes).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch residence types: %w", err)
|
||||
}
|
||||
if err := cache.CacheResidenceTypes(ctx, residenceTypes); err != nil {
|
||||
return false, fmt.Errorf("failed to cache residence types: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(residenceTypes)).Msg("Cached residence types")
|
||||
|
||||
// Fetch and cache contractor specialties
|
||||
var specialties []models.ContractorSpecialty
|
||||
if err := h.db.Order("display_order ASC, name ASC").Find(&specialties).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch specialties: %w", err)
|
||||
}
|
||||
if err := cache.CacheSpecialties(ctx, specialties); err != nil {
|
||||
return false, fmt.Errorf("failed to cache specialties: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(specialties)).Msg("Cached contractor specialties")
|
||||
|
||||
// Fetch and cache task templates (only active ones)
|
||||
var taskTemplates []models.TaskTemplate
|
||||
if err := h.db.Preload("Category").Preload("Frequency").
|
||||
Where("is_active = ?", true).
|
||||
Order("display_order ASC, title ASC").
|
||||
Find(&taskTemplates).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to fetch task templates: %w", err)
|
||||
}
|
||||
if err := cache.CacheTaskTemplates(ctx, taskTemplates); err != nil {
|
||||
return false, fmt.Errorf("failed to cache task templates: %w", err)
|
||||
}
|
||||
log.Debug().Int("count", len(taskTemplates)).Msg("Cached task templates")
|
||||
|
||||
// Build and cache the unified seeded data response
|
||||
// Import the grouped response type
|
||||
seededData := map[string]interface{}{
|
||||
"residence_types": residenceTypes,
|
||||
"task_categories": categories,
|
||||
"task_priorities": priorities,
|
||||
"task_frequencies": frequencies,
|
||||
"task_statuses": statuses,
|
||||
"contractor_specialties": specialties,
|
||||
"task_templates": buildGroupedTemplates(taskTemplates),
|
||||
}
|
||||
|
||||
etag, err := cache.CacheSeededData(ctx, seededData)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to cache seeded data: %w", err)
|
||||
}
|
||||
log.Debug().Str("etag", etag).Msg("Cached unified seeded data")
|
||||
|
||||
log.Info().Msg("All lookup data cached in Redis successfully")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// buildGroupedTemplates groups task templates by category for the seeded data response
|
||||
func buildGroupedTemplates(templates []models.TaskTemplate) map[string]interface{} {
|
||||
type templateResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
Category map[string]interface{} `json:"category,omitempty"`
|
||||
FrequencyID *uint `json:"frequency_id"`
|
||||
Frequency map[string]interface{} `json:"frequency,omitempty"`
|
||||
IconIOS string `json:"icon_ios"`
|
||||
IconAndroid string `json:"icon_android"`
|
||||
Tags []string `json:"tags"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type categoryGroup struct {
|
||||
CategoryName string `json:"category_name"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
Templates []templateResponse `json:"templates"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
categoryMap := make(map[string]*categoryGroup)
|
||||
categoryOrder := []string{}
|
||||
|
||||
for _, t := range templates {
|
||||
categoryName := "Uncategorized"
|
||||
var categoryID *uint
|
||||
if t.Category != nil {
|
||||
categoryName = t.Category.Name
|
||||
categoryID = &t.Category.ID
|
||||
}
|
||||
|
||||
if _, exists := categoryMap[categoryName]; !exists {
|
||||
categoryMap[categoryName] = &categoryGroup{
|
||||
CategoryName: categoryName,
|
||||
CategoryID: categoryID,
|
||||
Templates: []templateResponse{},
|
||||
}
|
||||
categoryOrder = append(categoryOrder, categoryName)
|
||||
}
|
||||
|
||||
resp := templateResponse{
|
||||
ID: t.ID,
|
||||
Title: t.Title,
|
||||
Description: t.Description,
|
||||
CategoryID: t.CategoryID,
|
||||
FrequencyID: t.FrequencyID,
|
||||
IconIOS: t.IconIOS,
|
||||
IconAndroid: t.IconAndroid,
|
||||
Tags: parseTags(t.Tags),
|
||||
DisplayOrder: t.DisplayOrder,
|
||||
IsActive: t.IsActive,
|
||||
}
|
||||
|
||||
if t.Category != nil {
|
||||
resp.Category = map[string]interface{}{
|
||||
"id": t.Category.ID,
|
||||
"name": t.Category.Name,
|
||||
"description": t.Category.Description,
|
||||
"icon": t.Category.Icon,
|
||||
"color": t.Category.Color,
|
||||
"display_order": t.Category.DisplayOrder,
|
||||
}
|
||||
}
|
||||
if t.Frequency != nil {
|
||||
resp.Frequency = map[string]interface{}{
|
||||
"id": t.Frequency.ID,
|
||||
"name": t.Frequency.Name,
|
||||
"days": t.Frequency.Days,
|
||||
"display_order": t.Frequency.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
categoryMap[categoryName].Templates = append(categoryMap[categoryName].Templates, resp)
|
||||
}
|
||||
|
||||
categories := make([]categoryGroup, len(categoryOrder))
|
||||
totalCount := 0
|
||||
for i, name := range categoryOrder {
|
||||
group := categoryMap[name]
|
||||
group.Count = len(group.Templates)
|
||||
totalCount += group.Count
|
||||
categories[i] = *group
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"categories": categories,
|
||||
"total_count": totalCount,
|
||||
}
|
||||
}
|
||||
|
||||
// parseTags splits a comma-separated tags string into a slice
|
||||
func parseTags(tags string) []string {
|
||||
if tags == "" {
|
||||
return []string{}
|
||||
}
|
||||
parts := strings.Split(tags, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
trimmed := strings.TrimSpace(p)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SeedTestData handles POST /api/admin/settings/seed-test-data
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
// AdminTaskTemplateHandler handles admin task template management endpoints
|
||||
@@ -21,6 +24,36 @@ func NewAdminTaskTemplateHandler(db *gorm.DB) *AdminTaskTemplateHandler {
|
||||
return &AdminTaskTemplateHandler{db: db}
|
||||
}
|
||||
|
||||
// refreshTaskTemplatesCache invalidates and refreshes the task templates cache
|
||||
func (h *AdminTaskTemplateHandler) refreshTaskTemplatesCache(ctx context.Context) {
|
||||
cache := services.GetCache()
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var templates []models.TaskTemplate
|
||||
if err := h.db.Preload("Category").Preload("Frequency").
|
||||
Where("is_active = ?", true).
|
||||
Order("display_order ASC, title ASC").
|
||||
Find(&templates).Error; err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to fetch task templates for cache refresh")
|
||||
return
|
||||
}
|
||||
|
||||
if err := cache.CacheTaskTemplates(ctx, templates); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to cache task templates")
|
||||
return
|
||||
}
|
||||
log.Debug().Int("count", len(templates)).Msg("Refreshed task templates cache")
|
||||
|
||||
// Invalidate unified seeded data cache
|
||||
if err := cache.InvalidateSeededData(ctx); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to invalidate seeded data cache")
|
||||
return
|
||||
}
|
||||
log.Debug().Msg("Invalidated seeded data cache")
|
||||
}
|
||||
|
||||
// TaskTemplateResponse represents a task template in admin responses
|
||||
type TaskTemplateResponse struct {
|
||||
ID uint `json:"id"`
|
||||
@@ -145,6 +178,9 @@ func (h *AdminTaskTemplateHandler) CreateTemplate(c *gin.Context) {
|
||||
// Reload with preloads
|
||||
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
|
||||
|
||||
// Refresh cache after creating
|
||||
h.refreshTaskTemplatesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, h.toResponse(&template))
|
||||
}
|
||||
|
||||
@@ -195,6 +231,9 @@ func (h *AdminTaskTemplateHandler) UpdateTemplate(c *gin.Context) {
|
||||
// Reload with preloads
|
||||
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
|
||||
|
||||
// Refresh cache after updating
|
||||
h.refreshTaskTemplatesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, h.toResponse(&template))
|
||||
}
|
||||
|
||||
@@ -211,6 +250,9 @@ func (h *AdminTaskTemplateHandler) DeleteTemplate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after deleting
|
||||
h.refreshTaskTemplatesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Template deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -241,6 +283,9 @@ func (h *AdminTaskTemplateHandler) ToggleActive(c *gin.Context) {
|
||||
// Reload with preloads
|
||||
h.db.Preload("Category").Preload("Frequency").First(&template, template.ID)
|
||||
|
||||
// Refresh cache after toggling active status
|
||||
h.refreshTaskTemplatesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusOK, h.toResponse(&template))
|
||||
}
|
||||
|
||||
@@ -279,6 +324,9 @@ func (h *AdminTaskTemplateHandler) BulkCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh cache after bulk creating
|
||||
h.refreshTaskTemplatesCache(c.Request.Context())
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"message": "Templates created successfully", "count": len(templates)})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,32 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/dto/responses"
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
// SeededDataResponse represents the unified seeded data response
|
||||
type SeededDataResponse struct {
|
||||
ResidenceTypes interface{} `json:"residence_types"`
|
||||
TaskCategories interface{} `json:"task_categories"`
|
||||
TaskPriorities interface{} `json:"task_priorities"`
|
||||
TaskFrequencies interface{} `json:"task_frequencies"`
|
||||
TaskStatuses interface{} `json:"task_statuses"`
|
||||
ContractorSpecialties interface{} `json:"contractor_specialties"`
|
||||
TaskTemplates responses.TaskTemplatesGroupedResponse `json:"task_templates"`
|
||||
}
|
||||
|
||||
// StaticDataHandler handles static/lookup data endpoints
|
||||
type StaticDataHandler struct {
|
||||
residenceService *services.ResidenceService
|
||||
taskService *services.TaskService
|
||||
contractorService *services.ContractorService
|
||||
residenceService *services.ResidenceService
|
||||
taskService *services.TaskService
|
||||
contractorService *services.ContractorService
|
||||
taskTemplateService *services.TaskTemplateService
|
||||
cache *services.CacheService
|
||||
}
|
||||
|
||||
// NewStaticDataHandler creates a new static data handler
|
||||
@@ -21,18 +37,56 @@ func NewStaticDataHandler(
|
||||
residenceService *services.ResidenceService,
|
||||
taskService *services.TaskService,
|
||||
contractorService *services.ContractorService,
|
||||
taskTemplateService *services.TaskTemplateService,
|
||||
cache *services.CacheService,
|
||||
) *StaticDataHandler {
|
||||
return &StaticDataHandler{
|
||||
residenceService: residenceService,
|
||||
taskService: taskService,
|
||||
contractorService: contractorService,
|
||||
residenceService: residenceService,
|
||||
taskService: taskService,
|
||||
contractorService: contractorService,
|
||||
taskTemplateService: taskTemplateService,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// GetStaticData handles GET /api/static_data/
|
||||
// Returns all lookup/reference data in a single response
|
||||
// Returns all lookup/reference data in a single response with ETag support
|
||||
func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
||||
// Get all lookup data
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Check If-None-Match header for conditional request
|
||||
clientETag := c.GetHeader("If-None-Match")
|
||||
|
||||
// Try to get cached ETag first (fast path for 304 responses)
|
||||
if h.cache != nil && clientETag != "" {
|
||||
cachedETag, err := h.cache.GetSeededDataETag(ctx)
|
||||
if err == nil && cachedETag == clientETag {
|
||||
// Client has the latest data, return 304 Not Modified
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get cached seeded data
|
||||
if h.cache != nil {
|
||||
var cachedData SeededDataResponse
|
||||
err := h.cache.GetCachedSeededData(ctx, &cachedData)
|
||||
if err == nil {
|
||||
// Cache hit - get the ETag and return data
|
||||
etag, etagErr := h.cache.GetSeededDataETag(ctx)
|
||||
if etagErr == nil {
|
||||
c.Header("ETag", etag)
|
||||
c.Header("Cache-Control", "private, max-age=3600")
|
||||
}
|
||||
c.JSON(http.StatusOK, cachedData)
|
||||
return
|
||||
} else if err != redis.Nil {
|
||||
// Log cache error but continue to fetch from DB
|
||||
log.Warn().Err(err).Msg("Failed to get cached seeded data")
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss - fetch all data from services
|
||||
residenceTypes, err := h.residenceService.GetResidenceTypes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_residence_types")})
|
||||
@@ -69,14 +123,35 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"residence_types": residenceTypes,
|
||||
"task_categories": taskCategories,
|
||||
"task_priorities": taskPriorities,
|
||||
"task_frequencies": taskFrequencies,
|
||||
"task_statuses": taskStatuses,
|
||||
"contractor_specialties": contractorSpecialties,
|
||||
})
|
||||
taskTemplates, err := h.taskTemplateService.GetGrouped()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_templates")})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
seededData := SeededDataResponse{
|
||||
ResidenceTypes: residenceTypes,
|
||||
TaskCategories: taskCategories,
|
||||
TaskPriorities: taskPriorities,
|
||||
TaskFrequencies: taskFrequencies,
|
||||
TaskStatuses: taskStatuses,
|
||||
ContractorSpecialties: contractorSpecialties,
|
||||
TaskTemplates: taskTemplates,
|
||||
}
|
||||
|
||||
// Cache the data and get ETag
|
||||
if h.cache != nil {
|
||||
etag, cacheErr := h.cache.CacheSeededData(ctx, seededData)
|
||||
if cacheErr != nil {
|
||||
log.Warn().Err(cacheErr).Msg("Failed to cache seeded data")
|
||||
} else {
|
||||
c.Header("ETag", etag)
|
||||
c.Header("Cache-Control", "private, max-age=3600")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, seededData)
|
||||
}
|
||||
|
||||
// RefreshStaticData handles POST /api/static_data/refresh/
|
||||
|
||||
@@ -118,7 +118,7 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
|
||||
documentHandler := handlers.NewDocumentHandler(documentService, deps.StorageService)
|
||||
notificationHandler := handlers.NewNotificationHandler(notificationService)
|
||||
subscriptionHandler := handlers.NewSubscriptionHandler(subscriptionService)
|
||||
staticDataHandler := handlers.NewStaticDataHandler(residenceService, taskService, contractorService)
|
||||
staticDataHandler := handlers.NewStaticDataHandler(residenceService, taskService, contractorService, taskTemplateService, deps.Cache)
|
||||
taskTemplateHandler := handlers.NewTaskTemplateHandler(taskTemplateService)
|
||||
|
||||
// Initialize upload handler (if storage service is available)
|
||||
|
||||
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -161,3 +162,199 @@ func (c *CacheService) GetCachedStaticData(ctx context.Context, dest interface{}
|
||||
func (c *CacheService) InvalidateStaticData(ctx context.Context) error {
|
||||
return c.Delete(ctx, StaticDataKey)
|
||||
}
|
||||
|
||||
// Lookup data cache helpers - each lookup type gets its own key
|
||||
const (
|
||||
LookupKeyPrefix = "lookup:"
|
||||
LookupCategoriesKey = LookupKeyPrefix + "categories"
|
||||
LookupPrioritiesKey = LookupKeyPrefix + "priorities"
|
||||
LookupStatusesKey = LookupKeyPrefix + "statuses"
|
||||
LookupFrequenciesKey = LookupKeyPrefix + "frequencies"
|
||||
LookupResidenceTypesKey = LookupKeyPrefix + "residence_types"
|
||||
LookupSpecialtiesKey = LookupKeyPrefix + "specialties"
|
||||
LookupTaskTemplatesKey = LookupKeyPrefix + "task_templates"
|
||||
LookupDataTTL = 24 * time.Hour // Lookup data rarely changes
|
||||
)
|
||||
|
||||
// CacheLookupData caches data for a specific lookup type
|
||||
func (c *CacheService) CacheLookupData(ctx context.Context, key string, data interface{}) error {
|
||||
return c.Set(ctx, key, data, LookupDataTTL)
|
||||
}
|
||||
|
||||
// GetCachedLookupData retrieves cached lookup data for a specific key
|
||||
func (c *CacheService) GetCachedLookupData(ctx context.Context, key string, dest interface{}) error {
|
||||
return c.Get(ctx, key, dest)
|
||||
}
|
||||
|
||||
// InvalidateLookupData removes cached data for a specific lookup type
|
||||
func (c *CacheService) InvalidateLookupData(ctx context.Context, key string) error {
|
||||
return c.Delete(ctx, key)
|
||||
}
|
||||
|
||||
// InvalidateAllLookups removes all cached lookup data
|
||||
func (c *CacheService) InvalidateAllLookups(ctx context.Context) error {
|
||||
keys := []string{
|
||||
LookupCategoriesKey,
|
||||
LookupPrioritiesKey,
|
||||
LookupStatusesKey,
|
||||
LookupFrequenciesKey,
|
||||
LookupResidenceTypesKey,
|
||||
LookupSpecialtiesKey,
|
||||
LookupTaskTemplatesKey,
|
||||
StaticDataKey, // Also invalidate the combined static data
|
||||
SeededDataKey, // Invalidate unified seeded data
|
||||
SeededDataETagKey, // Invalidate seeded data ETag
|
||||
}
|
||||
return c.Delete(ctx, keys...)
|
||||
}
|
||||
|
||||
// CacheCategories caches task categories
|
||||
func (c *CacheService) CacheCategories(ctx context.Context, data interface{}) error {
|
||||
return c.CacheLookupData(ctx, LookupCategoriesKey, data)
|
||||
}
|
||||
|
||||
// GetCachedCategories retrieves cached task categories
|
||||
func (c *CacheService) GetCachedCategories(ctx context.Context, dest interface{}) error {
|
||||
return c.GetCachedLookupData(ctx, LookupCategoriesKey, dest)
|
||||
}
|
||||
|
||||
// InvalidateCategories removes cached task categories
|
||||
func (c *CacheService) InvalidateCategories(ctx context.Context) error {
|
||||
// Invalidate both specific key and combined static data
|
||||
return c.Delete(ctx, LookupCategoriesKey, StaticDataKey)
|
||||
}
|
||||
|
||||
// CachePriorities caches task priorities
|
||||
func (c *CacheService) CachePriorities(ctx context.Context, data interface{}) error {
|
||||
return c.CacheLookupData(ctx, LookupPrioritiesKey, data)
|
||||
}
|
||||
|
||||
// GetCachedPriorities retrieves cached task priorities
|
||||
func (c *CacheService) GetCachedPriorities(ctx context.Context, dest interface{}) error {
|
||||
return c.GetCachedLookupData(ctx, LookupPrioritiesKey, dest)
|
||||
}
|
||||
|
||||
// InvalidatePriorities removes cached task priorities
|
||||
func (c *CacheService) InvalidatePriorities(ctx context.Context) error {
|
||||
return c.Delete(ctx, LookupPrioritiesKey, StaticDataKey)
|
||||
}
|
||||
|
||||
// CacheStatuses caches task statuses
|
||||
func (c *CacheService) CacheStatuses(ctx context.Context, data interface{}) error {
|
||||
return c.CacheLookupData(ctx, LookupStatusesKey, data)
|
||||
}
|
||||
|
||||
// GetCachedStatuses retrieves cached task statuses
|
||||
func (c *CacheService) GetCachedStatuses(ctx context.Context, dest interface{}) error {
|
||||
return c.GetCachedLookupData(ctx, LookupStatusesKey, dest)
|
||||
}
|
||||
|
||||
// InvalidateStatuses removes cached task statuses
|
||||
func (c *CacheService) InvalidateStatuses(ctx context.Context) error {
|
||||
return c.Delete(ctx, LookupStatusesKey, StaticDataKey)
|
||||
}
|
||||
|
||||
// CacheFrequencies caches task frequencies
|
||||
func (c *CacheService) CacheFrequencies(ctx context.Context, data interface{}) error {
|
||||
return c.CacheLookupData(ctx, LookupFrequenciesKey, data)
|
||||
}
|
||||
|
||||
// GetCachedFrequencies retrieves cached task frequencies
|
||||
func (c *CacheService) GetCachedFrequencies(ctx context.Context, dest interface{}) error {
|
||||
return c.GetCachedLookupData(ctx, LookupFrequenciesKey, dest)
|
||||
}
|
||||
|
||||
// InvalidateFrequencies removes cached task frequencies
|
||||
func (c *CacheService) InvalidateFrequencies(ctx context.Context) error {
|
||||
return c.Delete(ctx, LookupFrequenciesKey, StaticDataKey)
|
||||
}
|
||||
|
||||
// CacheResidenceTypes caches residence types
|
||||
func (c *CacheService) CacheResidenceTypes(ctx context.Context, data interface{}) error {
|
||||
return c.CacheLookupData(ctx, LookupResidenceTypesKey, data)
|
||||
}
|
||||
|
||||
// GetCachedResidenceTypes retrieves cached residence types
|
||||
func (c *CacheService) GetCachedResidenceTypes(ctx context.Context, dest interface{}) error {
|
||||
return c.GetCachedLookupData(ctx, LookupResidenceTypesKey, dest)
|
||||
}
|
||||
|
||||
// InvalidateResidenceTypes removes cached residence types
|
||||
func (c *CacheService) InvalidateResidenceTypes(ctx context.Context) error {
|
||||
return c.Delete(ctx, LookupResidenceTypesKey, StaticDataKey)
|
||||
}
|
||||
|
||||
// CacheSpecialties caches contractor specialties
|
||||
func (c *CacheService) CacheSpecialties(ctx context.Context, data interface{}) error {
|
||||
return c.CacheLookupData(ctx, LookupSpecialtiesKey, data)
|
||||
}
|
||||
|
||||
// GetCachedSpecialties retrieves cached contractor specialties
|
||||
func (c *CacheService) GetCachedSpecialties(ctx context.Context, dest interface{}) error {
|
||||
return c.GetCachedLookupData(ctx, LookupSpecialtiesKey, dest)
|
||||
}
|
||||
|
||||
// InvalidateSpecialties removes cached contractor specialties
|
||||
func (c *CacheService) InvalidateSpecialties(ctx context.Context) error {
|
||||
return c.Delete(ctx, LookupSpecialtiesKey, StaticDataKey)
|
||||
}
|
||||
|
||||
// CacheTaskTemplates caches task templates
|
||||
func (c *CacheService) CacheTaskTemplates(ctx context.Context, data interface{}) error {
|
||||
return c.CacheLookupData(ctx, LookupTaskTemplatesKey, data)
|
||||
}
|
||||
|
||||
// GetCachedTaskTemplates retrieves cached task templates
|
||||
func (c *CacheService) GetCachedTaskTemplates(ctx context.Context, dest interface{}) error {
|
||||
return c.GetCachedLookupData(ctx, LookupTaskTemplatesKey, dest)
|
||||
}
|
||||
|
||||
// InvalidateTaskTemplates removes cached task templates
|
||||
func (c *CacheService) InvalidateTaskTemplates(ctx context.Context) error {
|
||||
return c.Delete(ctx, LookupTaskTemplatesKey, StaticDataKey)
|
||||
}
|
||||
|
||||
// Unified seeded data cache helpers
|
||||
const (
|
||||
SeededDataKey = "seeded_data"
|
||||
SeededDataETagKey = "seeded_data:etag"
|
||||
SeededDataTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
// CacheSeededData caches the unified seeded data and generates an ETag
|
||||
func (c *CacheService) CacheSeededData(ctx context.Context, data interface{}) (string, error) {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal seeded data: %w", err)
|
||||
}
|
||||
|
||||
// Generate MD5 ETag from the JSON data
|
||||
hash := md5.Sum(jsonData)
|
||||
etag := fmt.Sprintf("\"%x\"", hash)
|
||||
|
||||
// Store both the data and the ETag
|
||||
if err := c.client.Set(ctx, SeededDataKey, jsonData, SeededDataTTL).Err(); err != nil {
|
||||
return "", fmt.Errorf("failed to cache seeded data: %w", err)
|
||||
}
|
||||
|
||||
if err := c.client.Set(ctx, SeededDataETagKey, etag, SeededDataTTL).Err(); err != nil {
|
||||
return "", fmt.Errorf("failed to cache seeded data etag: %w", err)
|
||||
}
|
||||
|
||||
return etag, nil
|
||||
}
|
||||
|
||||
// GetCachedSeededData retrieves cached unified seeded data
|
||||
func (c *CacheService) GetCachedSeededData(ctx context.Context, dest interface{}) error {
|
||||
return c.Get(ctx, SeededDataKey, dest)
|
||||
}
|
||||
|
||||
// GetSeededDataETag retrieves the cached ETag for seeded data
|
||||
func (c *CacheService) GetSeededDataETag(ctx context.Context) (string, error) {
|
||||
return c.GetString(ctx, SeededDataETagKey)
|
||||
}
|
||||
|
||||
// InvalidateSeededData removes cached seeded data and its ETag
|
||||
func (c *CacheService) InvalidateSeededData(ctx context.Context) error {
|
||||
return c.Delete(ctx, SeededDataKey, SeededDataETagKey)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user