data layer

This commit is contained in:
2026-05-25 08:38:26 +07:00
parent 4e8c11d545
commit a428170fef
81 changed files with 3941 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
// 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)
})
}