88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
// Package service holds business logic. It owns cache orchestration around
|
|
// the read repos and never touches HTTP/chi or the SQL drivers directly.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/dbiz/cdp/data-layer/api/internal/apperr"
|
|
"github.com/dbiz/cdp/data-layer/api/internal/cache"
|
|
"github.com/dbiz/cdp/data-layer/api/internal/model"
|
|
"github.com/dbiz/cdp/data-layer/api/internal/repo"
|
|
)
|
|
|
|
type QueryService struct {
|
|
events *repo.EventRepo
|
|
analytics *repo.AnalyticsRepo
|
|
cache *cache.Cache
|
|
queryTTL time.Duration
|
|
log *zap.Logger
|
|
}
|
|
|
|
func NewQueryService(events *repo.EventRepo, analytics *repo.AnalyticsRepo, c *cache.Cache, queryTTL time.Duration, log *zap.Logger) *QueryService {
|
|
return &QueryService{events: events, analytics: analytics, cache: c, queryTTL: queryTTL, log: log}
|
|
}
|
|
|
|
// cached wraps `fetch` with the per-workspace Redis cache. Result is JSON-
|
|
// encoded on miss; CacheHit is set true on hit.
|
|
func (s *QueryService) cached(
|
|
ctx context.Context,
|
|
kind, workspaceID string,
|
|
params any,
|
|
fetch func(context.Context) (*model.QueryResult, error),
|
|
) (*model.QueryResult, error) {
|
|
key, err := cache.Key(kind, workspaceID, params)
|
|
if err != nil {
|
|
return nil, apperr.Internal(err)
|
|
}
|
|
if cached, ok := s.cache.Get(ctx, key); ok {
|
|
var out model.QueryResult
|
|
if jerr := json.Unmarshal(cached, &out); jerr == nil {
|
|
out.CacheHit = true
|
|
return &out, nil
|
|
}
|
|
}
|
|
start := time.Now()
|
|
res, err := fetch(ctx)
|
|
if err != nil {
|
|
return nil, apperr.Internal(err)
|
|
}
|
|
res.DurationMS = time.Since(start).Milliseconds()
|
|
res.CacheHit = false
|
|
|
|
if b, jerr := json.Marshal(res); jerr == nil {
|
|
if cerr := s.cache.Set(ctx, key, b, s.queryTTL); cerr != nil {
|
|
s.log.Warn("cache set", zap.String("key", key), zap.Error(cerr))
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *QueryService) Events(ctx context.Context, q model.EventQuery) (*model.QueryResult, error) {
|
|
return s.cached(ctx, "query:events", q.WorkspaceID, q, func(c context.Context) (*model.QueryResult, error) {
|
|
return s.events.QueryEvents(c, q)
|
|
})
|
|
}
|
|
|
|
func (s *QueryService) Funnel(ctx context.Context, q repo.FunnelQuery) (*model.QueryResult, error) {
|
|
return s.cached(ctx, "query:funnel", q.WorkspaceID, q, func(c context.Context) (*model.QueryResult, error) {
|
|
return s.analytics.Funnel(c, q)
|
|
})
|
|
}
|
|
|
|
func (s *QueryService) Retention(ctx context.Context, q repo.RetentionQuery) (*model.QueryResult, error) {
|
|
return s.cached(ctx, "query:retention", q.WorkspaceID, q, func(c context.Context) (*model.QueryResult, error) {
|
|
return s.analytics.Retention(c, q)
|
|
})
|
|
}
|
|
|
|
func (s *QueryService) Sessions(ctx context.Context, q repo.SessionQuery) (*model.QueryResult, error) {
|
|
return s.cached(ctx, "query:session", q.WorkspaceID, q, func(c context.Context) (*model.QueryResult, error) {
|
|
return s.analytics.Sessions(c, q)
|
|
})
|
|
}
|