mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
Merge pull request #12 from SamyRai/feature/complete-pending-tasks
feat: Complete All Pending Tasks
This commit is contained in:
commit
23a6b6d569
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@ -43,11 +44,11 @@ func runMigrations(gormDB *gorm.DB) error {
|
|||||||
_, b, _, _ := runtime.Caller(0)
|
_, b, _, _ := runtime.Caller(0)
|
||||||
migrationsDir := filepath.Join(filepath.Dir(b), "../../internal/data/migrations")
|
migrationsDir := filepath.Join(filepath.Dir(b), "../../internal/data/migrations")
|
||||||
|
|
||||||
log.LogInfo("Applying database migrations", log.F("directory", migrationsDir))
|
log.Info(fmt.Sprintf("Applying database migrations from %s", migrationsDir))
|
||||||
if err := goose.Up(sqlDB, migrationsDir); err != nil {
|
if err := goose.Up(sqlDB, migrationsDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.LogInfo("Database migrations applied successfully")
|
log.Info("Database migrations applied successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,15 +59,16 @@ func main() {
|
|||||||
|
|
||||||
// Initialize logger
|
// Initialize logger
|
||||||
log.Init("tercul-api", config.Cfg.Environment)
|
log.Init("tercul-api", config.Cfg.Environment)
|
||||||
|
obsLogger := observability.NewLogger("tercul-api", config.Cfg.Environment)
|
||||||
|
|
||||||
// Initialize OpenTelemetry Tracer Provider
|
// Initialize OpenTelemetry Tracer Provider
|
||||||
tp, err := observability.TracerProvider("tercul-api", config.Cfg.Environment)
|
tp, err := observability.TracerProvider("tercul-api", config.Cfg.Environment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogFatal("Failed to initialize OpenTelemetry tracer", log.F("error", err))
|
log.Fatal(err, "Failed to initialize OpenTelemetry tracer")
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := tp.Shutdown(context.Background()); err != nil {
|
if err := tp.Shutdown(context.Background()); err != nil {
|
||||||
log.LogError("Error shutting down tracer provider", log.F("error", err))
|
log.Error(err, "Error shutting down tracer provider")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -74,19 +76,17 @@ func main() {
|
|||||||
reg := prometheus.NewRegistry()
|
reg := prometheus.NewRegistry()
|
||||||
metrics := observability.NewMetrics(reg) // Metrics are registered automatically
|
metrics := observability.NewMetrics(reg) // Metrics are registered automatically
|
||||||
|
|
||||||
log.LogInfo("Starting Tercul application",
|
log.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", config.Cfg.Environment))
|
||||||
log.F("environment", config.Cfg.Environment),
|
|
||||||
log.F("version", "1.0.0"))
|
|
||||||
|
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
database, err := db.InitDB()
|
database, err := db.InitDB(metrics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogFatal("Failed to initialize database", log.F("error", err))
|
log.Fatal(err, "Failed to initialize database")
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
if err := runMigrations(database); err != nil {
|
if err := runMigrations(database); err != nil {
|
||||||
log.LogFatal("Failed to apply database migrations", log.F("error", err))
|
log.Fatal(err, "Failed to apply database migrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Weaviate client
|
// Initialize Weaviate client
|
||||||
@ -96,7 +96,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
weaviateClient, err := weaviate.NewClient(weaviateCfg)
|
weaviateClient, err := weaviate.NewClient(weaviateCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogFatal("Failed to create weaviate client", log.F("error", err))
|
log.Fatal(err, "Failed to create weaviate client")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create search client
|
// Create search client
|
||||||
@ -109,7 +109,7 @@ func main() {
|
|||||||
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
|
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
|
||||||
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogFatal("Failed to create sentiment provider", log.F("error", err))
|
log.Fatal(err, "Failed to create sentiment provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create application services
|
// Create application services
|
||||||
@ -124,12 +124,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jwtManager := auth.NewJWTManager()
|
jwtManager := auth.NewJWTManager()
|
||||||
srv := NewServerWithAuth(resolver, jwtManager, metrics)
|
srv := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
|
||||||
graphQLServer := &http.Server{
|
graphQLServer := &http.Server{
|
||||||
Addr: config.Cfg.ServerPort,
|
Addr: config.Cfg.ServerPort,
|
||||||
Handler: srv,
|
Handler: srv,
|
||||||
}
|
}
|
||||||
log.LogInfo("GraphQL server created successfully", log.F("port", config.Cfg.ServerPort))
|
log.Info(fmt.Sprintf("GraphQL server created successfully on port %s", config.Cfg.ServerPort))
|
||||||
|
|
||||||
// Create GraphQL playground
|
// Create GraphQL playground
|
||||||
playgroundHandler := playground.Handler("GraphQL", "/query")
|
playgroundHandler := playground.Handler("GraphQL", "/query")
|
||||||
@ -137,38 +137,34 @@ func main() {
|
|||||||
Addr: config.Cfg.PlaygroundPort,
|
Addr: config.Cfg.PlaygroundPort,
|
||||||
Handler: playgroundHandler,
|
Handler: playgroundHandler,
|
||||||
}
|
}
|
||||||
log.LogInfo("GraphQL playground created successfully", log.F("port", config.Cfg.PlaygroundPort))
|
log.Info(fmt.Sprintf("GraphQL playground created successfully on port %s", config.Cfg.PlaygroundPort))
|
||||||
|
|
||||||
// Create metrics server
|
// Create metrics server
|
||||||
metricsServer := &http.Server{
|
metricsServer := &http.Server{
|
||||||
Addr: ":9090",
|
Addr: ":9090",
|
||||||
Handler: observability.PrometheusHandler(reg),
|
Handler: observability.PrometheusHandler(reg),
|
||||||
}
|
}
|
||||||
log.LogInfo("Metrics server created successfully", log.F("port", ":9090"))
|
log.Info("Metrics server created successfully on port :9090")
|
||||||
|
|
||||||
// Start HTTP servers in goroutines
|
// Start HTTP servers in goroutines
|
||||||
go func() {
|
go func() {
|
||||||
log.LogInfo("Starting GraphQL server",
|
log.Info(fmt.Sprintf("Starting GraphQL server on port %s", config.Cfg.ServerPort))
|
||||||
log.F("port", config.Cfg.ServerPort))
|
|
||||||
if err := graphQLServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := graphQLServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.LogFatal("Failed to start GraphQL server",
|
log.Fatal(err, "Failed to start GraphQL server")
|
||||||
log.F("error", err))
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.LogInfo("Starting GraphQL playground",
|
log.Info(fmt.Sprintf("Starting GraphQL playground on port %s", config.Cfg.PlaygroundPort))
|
||||||
log.F("port", config.Cfg.PlaygroundPort))
|
|
||||||
if err := playgroundServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := playgroundServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.LogFatal("Failed to start GraphQL playground",
|
log.Fatal(err, "Failed to start GraphQL playground")
|
||||||
log.F("error", err))
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.LogInfo("Starting metrics server", log.F("port", ":9090"))
|
log.Info("Starting metrics server on port :9090")
|
||||||
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.LogFatal("Failed to start metrics server", log.F("error", err))
|
log.Fatal(err, "Failed to start metrics server")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -177,25 +173,23 @@ func main() {
|
|||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
|
|
||||||
log.LogInfo("Shutting down servers...")
|
log.Info("Shutting down servers...")
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := graphQLServer.Shutdown(ctx); err != nil {
|
if err := graphQLServer.Shutdown(ctx); err != nil {
|
||||||
log.LogError("GraphQL server forced to shutdown",
|
log.Error(err, "GraphQL server forced to shutdown")
|
||||||
log.F("error", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := playgroundServer.Shutdown(ctx); err != nil {
|
if err := playgroundServer.Shutdown(ctx); err != nil {
|
||||||
log.LogError("GraphQL playground forced to shutdown",
|
log.Error(err, "GraphQL playground forced to shutdown")
|
||||||
log.F("error", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := metricsServer.Shutdown(ctx); err != nil {
|
if err := metricsServer.Shutdown(ctx); err != nil {
|
||||||
log.LogError("Metrics server forced to shutdown", log.F("error", err))
|
log.Error(err, "Metrics server forced to shutdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("All servers shutdown successfully")
|
log.Info("All servers shutdown successfully")
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@ func NewServer(resolver *graphql.Resolver) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewServerWithAuth creates a new GraphQL server with authentication and observability middleware
|
// NewServerWithAuth creates a new GraphQL server with authentication and observability middleware
|
||||||
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager, metrics *observability.Metrics) http.Handler {
|
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager, metrics *observability.Metrics, logger *observability.Logger) http.Handler {
|
||||||
c := graphql.Config{Resolvers: resolver}
|
c := graphql.Config{Resolvers: resolver}
|
||||||
c.Directives.Binding = graphql.Binding
|
c.Directives.Binding = graphql.Binding
|
||||||
|
|
||||||
@ -31,11 +31,14 @@ func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager,
|
|||||||
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
|
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
|
||||||
srv.SetErrorPresenter(graphql.NewErrorPresenter())
|
srv.SetErrorPresenter(graphql.NewErrorPresenter())
|
||||||
|
|
||||||
// Create a middleware chain
|
// Create a middleware chain. The order is important.
|
||||||
|
// Middlewares are applied from bottom to top, so the last one added is the first to run.
|
||||||
var chain http.Handler
|
var chain http.Handler
|
||||||
chain = srv
|
chain = srv
|
||||||
chain = auth.GraphQLAuthMiddleware(jwtManager)(chain)
|
|
||||||
chain = metrics.PrometheusMiddleware(chain)
|
chain = metrics.PrometheusMiddleware(chain)
|
||||||
|
// LoggingMiddleware needs to run after auth and tracing to get all context.
|
||||||
|
chain = observability.LoggingMiddleware(logger)(chain)
|
||||||
|
chain = auth.GraphQLAuthMiddleware(jwtManager)(chain)
|
||||||
chain = observability.TracingMiddleware(chain)
|
chain = observability.TracingMiddleware(chain)
|
||||||
chain = observability.RequestIDMiddleware(chain)
|
chain = observability.RequestIDMiddleware(chain)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,69 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/enrichment"
|
||||||
|
"tercul/internal/platform/config"
|
||||||
|
"tercul/internal/platform/db"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// TODO: Fix this tool
|
// 1. Parse command-line arguments
|
||||||
|
entityType := flag.String("type", "", "The type of entity to enrich (e.g., 'author')")
|
||||||
|
entityIDStr := flag.String("id", "", "The ID of the entity to enrich")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *entityType == "" || *entityIDStr == "" {
|
||||||
|
fmt.Println("Usage: go run cmd/tools/enrich/main.go --type <entity_type> --id <entity_id>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
entityID, err := strconv.ParseUint(*entityIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Invalid entity ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Initialize dependencies
|
||||||
|
config.LoadConfig()
|
||||||
|
log.Init("enrich-tool", "development")
|
||||||
|
database, err := db.InitDB(nil) // No metrics needed for this tool
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err, "Failed to initialize database")
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
repos := sql.NewRepositories(database)
|
||||||
|
enrichmentSvc := enrichment.NewService()
|
||||||
|
|
||||||
|
// 3. Fetch, enrich, and save the entity
|
||||||
|
ctx := context.Background()
|
||||||
|
log.Info(fmt.Sprintf("Enriching %s with ID %d", *entityType, entityID))
|
||||||
|
|
||||||
|
switch *entityType {
|
||||||
|
case "author":
|
||||||
|
author, err := repos.Author.GetByID(ctx, uint(entityID))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err, "Failed to get author")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := enrichmentSvc.EnrichAuthor(ctx, author); err != nil {
|
||||||
|
log.Fatal(err, "Failed to enrich author")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repos.Author.Update(ctx, author); err != nil {
|
||||||
|
log.Fatal(err, "Failed to save enriched author")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Successfully enriched and saved author")
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatal(fmt.Errorf("unknown entity type: %s", *entityType), "Enrichment failed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ func NewErrorPresenter() graphql.ErrorPresenterFunc {
|
|||||||
|
|
||||||
// Check for custom application errors and format them.
|
// Check for custom application errors and format them.
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(originalErr, domain.ErrNotFound):
|
case errors.Is(originalErr, domain.ErrEntityNotFound):
|
||||||
gqlErr.Message = "The requested resource was not found."
|
gqlErr.Message = "The requested resource was not found."
|
||||||
gqlErr.Extensions = map[string]interface{}{"code": "NOT_FOUND"}
|
gqlErr.Extensions = map[string]interface{}{"code": "NOT_FOUND"}
|
||||||
case errors.Is(originalErr, domain.ErrUnauthorized):
|
case errors.Is(originalErr, domain.ErrUnauthorized):
|
||||||
|
|||||||
@ -34,8 +34,8 @@ func (s *LikeResolversUnitSuite) SetupTest() {
|
|||||||
s.mockAnalyticsSvc = new(mockAnalyticsService)
|
s.mockAnalyticsSvc = new(mockAnalyticsService)
|
||||||
|
|
||||||
// 2. Create real services with mock repositories
|
// 2. Create real services with mock repositories
|
||||||
likeService := like.NewService(s.mockLikeRepo)
|
|
||||||
analyticsService := analytics.NewService(s.mockAnalyticsSvc, nil, nil, s.mockWorkRepo, nil)
|
analyticsService := analytics.NewService(s.mockAnalyticsSvc, nil, nil, s.mockWorkRepo, nil)
|
||||||
|
likeService := like.NewService(s.mockLikeRepo, analyticsService)
|
||||||
|
|
||||||
// 3. Create the resolver with the services
|
// 3. Create the resolver with the services
|
||||||
s.resolver = &graphql.Resolver{
|
s.resolver = &graphql.Resolver{
|
||||||
|
|||||||
@ -11,6 +11,9 @@ import (
|
|||||||
"tercul/internal/jobs/linguistics"
|
"tercul/internal/jobs/linguistics"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
@ -44,6 +47,7 @@ type service struct {
|
|||||||
translationRepo domain.TranslationRepository
|
translationRepo domain.TranslationRepository
|
||||||
workRepo work.WorkRepository
|
workRepo work.WorkRepository
|
||||||
sentimentProvider linguistics.SentimentProvider
|
sentimentProvider linguistics.SentimentProvider
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(repo Repository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, workRepo work.WorkRepository, sentimentProvider linguistics.SentimentProvider) Service {
|
func NewService(repo Repository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, workRepo work.WorkRepository, sentimentProvider linguistics.SentimentProvider) Service {
|
||||||
@ -53,58 +57,85 @@ func NewService(repo Repository, analysisRepo linguistics.AnalysisRepository, tr
|
|||||||
translationRepo: translationRepo,
|
translationRepo: translationRepo,
|
||||||
workRepo: workRepo,
|
workRepo: workRepo,
|
||||||
sentimentProvider: sentimentProvider,
|
sentimentProvider: sentimentProvider,
|
||||||
|
tracer: otel.Tracer("analytics.service"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkViews")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "views", 1)
|
return s.repo.IncrementWorkCounter(ctx, workID, "views", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkLikes")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
|
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkComments")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1)
|
return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkBookmarks")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
|
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkShares")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1)
|
return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkTranslationCount")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1)
|
return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationViews")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationLikes")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationComments")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationShares")
|
||||||
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
|
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "GetOrCreateWorkStats")
|
||||||
|
defer span.End()
|
||||||
return s.repo.GetOrCreateWorkStats(ctx, workID)
|
return s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "GetOrCreateTranslationStats")
|
||||||
|
defer span.End()
|
||||||
return s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
return s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "UpdateWorkReadingTime")
|
||||||
|
defer span.End()
|
||||||
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -130,6 +161,9 @@ func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "UpdateWorkComplexity")
|
||||||
|
defer span.End()
|
||||||
|
logger := log.FromContext(ctx).With("workID", workID)
|
||||||
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -137,7 +171,7 @@ func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
|||||||
|
|
||||||
_, readabilityScore, _, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
_, readabilityScore, _, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("could not get readability score for work", log.F("workID", workID), log.F("error", err))
|
logger.Error(err, "could not get readability score for work")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +185,9 @@ func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "UpdateWorkSentiment")
|
||||||
|
defer span.End()
|
||||||
|
logger := log.FromContext(ctx).With("workID", workID)
|
||||||
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -158,7 +195,7 @@ func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
|||||||
|
|
||||||
_, _, languageAnalysis, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
_, _, languageAnalysis, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("could not get language analysis for work", log.F("workID", workID), log.F("error", err))
|
logger.Error(err, "could not get language analysis for work")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,6 +214,8 @@ func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "UpdateTranslationReadingTime")
|
||||||
|
defer span.End()
|
||||||
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -203,6 +242,8 @@ func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "UpdateTranslationSentiment")
|
||||||
|
defer span.End()
|
||||||
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -228,6 +269,8 @@ func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "UpdateUserEngagement")
|
||||||
|
defer span.End()
|
||||||
today := time.Now().UTC().Truncate(24 * time.Hour)
|
today := time.Now().UTC().Truncate(24 * time.Hour)
|
||||||
engagement, err := s.repo.GetOrCreateUserEngagement(ctx, userID, today)
|
engagement, err := s.repo.GetOrCreateUserEngagement(ctx, userID, today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -253,11 +296,16 @@ func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventTy
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
|
func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "GetTrendingWorks")
|
||||||
|
defer span.End()
|
||||||
return s.repo.GetTrendingWorks(ctx, timePeriod, limit)
|
return s.repo.GetTrendingWorks(ctx, timePeriod, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateTrending(ctx context.Context) error {
|
func (s *service) UpdateTrending(ctx context.Context) error {
|
||||||
log.LogInfo("Updating trending works")
|
ctx, span := s.tracer.Start(ctx, "UpdateTrending")
|
||||||
|
defer span.End()
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
logger.Info("Updating trending works")
|
||||||
|
|
||||||
works, err := s.workRepo.ListAll(ctx)
|
works, err := s.workRepo.ListAll(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -268,7 +316,7 @@ func (s *service) UpdateTrending(ctx context.Context) error {
|
|||||||
for _, aWork := range works {
|
for _, aWork := range works {
|
||||||
stats, err := s.repo.GetOrCreateWorkStats(ctx, aWork.ID)
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, aWork.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("failed to get work stats", log.F("workID", aWork.ID), log.F("error", err))
|
logger.With("workID", aWork.ID).Error(err, "failed to get work stats")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,11 +46,11 @@ func NewApplication(repos *sql.Repositories, searchClient search.SearchClient, a
|
|||||||
authzService := authz.NewService(repos.Work, repos.Translation)
|
authzService := authz.NewService(repos.Work, repos.Translation)
|
||||||
authorService := author.NewService(repos.Author)
|
authorService := author.NewService(repos.Author)
|
||||||
bookService := book.NewService(repos.Book, authzService)
|
bookService := book.NewService(repos.Book, authzService)
|
||||||
bookmarkService := bookmark.NewService(repos.Bookmark)
|
bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
|
||||||
categoryService := category.NewService(repos.Category)
|
categoryService := category.NewService(repos.Category)
|
||||||
collectionService := collection.NewService(repos.Collection)
|
collectionService := collection.NewService(repos.Collection)
|
||||||
commentService := comment.NewService(repos.Comment, authzService)
|
commentService := comment.NewService(repos.Comment, authzService, analyticsService)
|
||||||
likeService := like.NewService(repos.Like)
|
likeService := like.NewService(repos.Like, analyticsService)
|
||||||
tagService := tag.NewService(repos.Tag)
|
tagService := tag.NewService(repos.Tag)
|
||||||
translationService := translation.NewService(repos.Translation, authzService)
|
translationService := translation.NewService(repos.Translation, authzService)
|
||||||
userService := user.NewService(repos.User, authzService)
|
userService := user.NewService(repos.User, authzService)
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -45,6 +47,7 @@ type AuthResponse struct {
|
|||||||
type AuthCommands struct {
|
type AuthCommands struct {
|
||||||
userRepo domain.UserRepository
|
userRepo domain.UserRepository
|
||||||
jwtManager auth.JWTManagement
|
jwtManager auth.JWTManagement
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthCommands creates a new AuthCommands handler.
|
// NewAuthCommands creates a new AuthCommands handler.
|
||||||
@ -52,48 +55,55 @@ func NewAuthCommands(userRepo domain.UserRepository, jwtManager auth.JWTManageme
|
|||||||
return &AuthCommands{
|
return &AuthCommands{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
jwtManager: jwtManager,
|
jwtManager: jwtManager,
|
||||||
|
tracer: otel.Tracer("auth.commands"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login authenticates a user and returns a JWT token
|
// Login authenticates a user and returns a JWT token
|
||||||
func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) {
|
func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "Login")
|
||||||
|
defer span.End()
|
||||||
|
logger := log.FromContext(ctx).With("email", input.Email)
|
||||||
|
|
||||||
if err := validateLoginInput(input); err != nil {
|
if err := validateLoginInput(input); err != nil {
|
||||||
log.LogWarn("Login validation failed", log.F("email", input.Email), log.F("error", err))
|
logger.Warn("Login validation failed")
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
email := strings.TrimSpace(input.Email)
|
email := strings.TrimSpace(input.Email)
|
||||||
log.LogDebug("Attempting to log in user", log.F("email", email))
|
logger.Debug("Attempting to log in user")
|
||||||
user, err := c.userRepo.FindByEmail(ctx, email)
|
user, err := c.userRepo.FindByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Login failed - user not found", log.F("email", email))
|
logger.Warn("Login failed - user not found")
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger = logger.With("user_id", user.ID)
|
||||||
|
|
||||||
if !user.Active {
|
if !user.Active {
|
||||||
log.LogWarn("Login failed - user inactive", log.F("user_id", user.ID), log.F("email", email))
|
logger.Warn("Login failed - user inactive")
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.CheckPassword(input.Password) {
|
if !user.CheckPassword(input.Password) {
|
||||||
log.LogWarn("Login failed - invalid password", log.F("user_id", user.ID), log.F("email", email))
|
logger.Warn("Login failed - invalid password")
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := c.jwtManager.GenerateToken(user)
|
token, err := c.jwtManager.GenerateToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to generate JWT token", log.F("user_id", user.ID), log.F("error", err))
|
logger.Error(err, "Failed to generate JWT token")
|
||||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
user.LastLoginAt = &now
|
user.LastLoginAt = &now
|
||||||
if err := c.userRepo.Update(ctx, user); err != nil {
|
if err := c.userRepo.Update(ctx, user); err != nil {
|
||||||
log.LogWarn("Failed to update last login time", log.F("user_id", user.ID), log.F("error", err))
|
logger.Error(err, "Failed to update last login time")
|
||||||
// Do not fail the login if this update fails
|
// Do not fail the login if this update fails
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("User logged in successfully", log.F("user_id", user.ID), log.F("email", email))
|
logger.Info("User logged in successfully")
|
||||||
return &AuthResponse{
|
return &AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
User: user,
|
User: user,
|
||||||
@ -103,24 +113,28 @@ func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthRespon
|
|||||||
|
|
||||||
// Register creates a new user account
|
// Register creates a new user account
|
||||||
func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) {
|
func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "Register")
|
||||||
|
defer span.End()
|
||||||
|
logger := log.FromContext(ctx).With("email", input.Email).With("username", input.Username)
|
||||||
|
|
||||||
if err := validateRegisterInput(input); err != nil {
|
if err := validateRegisterInput(input); err != nil {
|
||||||
log.LogWarn("Registration validation failed", log.F("email", input.Email), log.F("error", err))
|
logger.Warn("Registration validation failed")
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
email := strings.TrimSpace(input.Email)
|
email := strings.TrimSpace(input.Email)
|
||||||
username := strings.TrimSpace(input.Username)
|
username := strings.TrimSpace(input.Username)
|
||||||
log.LogDebug("Attempting to register new user", log.F("email", email), log.F("username", username))
|
logger.Debug("Attempting to register new user")
|
||||||
|
|
||||||
existingUser, _ := c.userRepo.FindByEmail(ctx, email)
|
existingUser, _ := c.userRepo.FindByEmail(ctx, email)
|
||||||
if existingUser != nil {
|
if existingUser != nil {
|
||||||
log.LogWarn("Registration failed - email already exists", log.F("email", email))
|
logger.Warn("Registration failed - email already exists")
|
||||||
return nil, ErrUserAlreadyExists
|
return nil, ErrUserAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
existingUser, _ = c.userRepo.FindByUsername(ctx, username)
|
existingUser, _ = c.userRepo.FindByUsername(ctx, username)
|
||||||
if existingUser != nil {
|
if existingUser != nil {
|
||||||
log.LogWarn("Registration failed - username already exists", log.F("username", username))
|
logger.Warn("Registration failed - username already exists")
|
||||||
return nil, ErrUserAlreadyExists
|
return nil, ErrUserAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,17 +151,19 @@ func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*Auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := c.userRepo.Create(ctx, user); err != nil {
|
if err := c.userRepo.Create(ctx, user); err != nil {
|
||||||
log.LogError("Failed to create user", log.F("email", email), log.F("error", err))
|
logger.Error(err, "Failed to create user")
|
||||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger = logger.With("user_id", user.ID)
|
||||||
|
|
||||||
token, err := c.jwtManager.GenerateToken(user)
|
token, err := c.jwtManager.GenerateToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to generate JWT token for new user", log.F("user_id", user.ID), log.F("error", err))
|
logger.Error(err, "Failed to generate JWT token for new user")
|
||||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("User registered successfully", log.F("user_id", user.ID))
|
logger.Info("User registered successfully")
|
||||||
return &AuthResponse{
|
return &AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
User: user,
|
User: user,
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/auth"
|
"tercul/internal/platform/auth"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -17,6 +20,7 @@ var (
|
|||||||
type AuthQueries struct {
|
type AuthQueries struct {
|
||||||
userRepo domain.UserRepository
|
userRepo domain.UserRepository
|
||||||
jwtManager auth.JWTManagement
|
jwtManager auth.JWTManagement
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthQueries creates a new AuthQueries handler.
|
// NewAuthQueries creates a new AuthQueries handler.
|
||||||
@ -24,6 +28,7 @@ func NewAuthQueries(userRepo domain.UserRepository, jwtManager auth.JWTManagemen
|
|||||||
return &AuthQueries{
|
return &AuthQueries{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
jwtManager: jwtManager,
|
jwtManager: jwtManager,
|
||||||
|
tracer: otel.Tracer("auth.queries"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,27 +37,31 @@ func (q *AuthQueries) GetUserFromContext(ctx context.Context) (*domain.User, err
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return nil, ErrContextRequired
|
return nil, ErrContextRequired
|
||||||
}
|
}
|
||||||
log.LogDebug("Attempting to get user from context")
|
ctx, span := q.tracer.Start(ctx, "GetUserFromContext")
|
||||||
|
defer span.End()
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
logger.Debug("Attempting to get user from context")
|
||||||
|
|
||||||
claims, err := auth.RequireAuth(ctx)
|
claims, err := auth.RequireAuth(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Failed to get user from context - authentication required", log.F("error", err))
|
logger.Warn("Failed to get user from context - authentication required")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.LogDebug("Claims found in context", log.F("user_id", claims.UserID))
|
logger = logger.With("user_id", claims.UserID)
|
||||||
|
logger.Debug("Claims found in context")
|
||||||
|
|
||||||
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Failed to get user from context - user not found", log.F("user_id", claims.UserID), log.F("error", err))
|
logger.Warn("Failed to get user from context - user not found")
|
||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.Active {
|
if !user.Active {
|
||||||
log.LogWarn("Failed to get user from context - user inactive", log.F("user_id", user.ID))
|
logger.Warn("Failed to get user from context - user inactive")
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("User retrieved from context successfully", log.F("user_id", user.ID))
|
logger.Debug("User retrieved from context successfully")
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,31 +70,36 @@ func (q *AuthQueries) ValidateToken(ctx context.Context, tokenString string) (*d
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return nil, ErrContextRequired
|
return nil, ErrContextRequired
|
||||||
}
|
}
|
||||||
|
ctx, span := q.tracer.Start(ctx, "ValidateToken")
|
||||||
|
defer span.End()
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
if tokenString == "" {
|
if tokenString == "" {
|
||||||
log.LogWarn("Token validation failed - empty token")
|
logger.Warn("Token validation failed - empty token")
|
||||||
return nil, auth.ErrMissingToken
|
return nil, auth.ErrMissingToken
|
||||||
}
|
}
|
||||||
log.LogDebug("Attempting to validate token")
|
logger.Debug("Attempting to validate token")
|
||||||
|
|
||||||
claims, err := q.jwtManager.ValidateToken(tokenString)
|
claims, err := q.jwtManager.ValidateToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Token validation failed - invalid token", log.F("error", err))
|
logger.Error(err, "Token validation failed - invalid token")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.LogDebug("Token claims validated", log.F("user_id", claims.UserID))
|
|
||||||
|
logger = logger.With("user_id", claims.UserID)
|
||||||
|
logger.Debug("Token claims validated")
|
||||||
|
|
||||||
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Token validation failed - user not found", log.F("user_id", claims.UserID), log.F("error", err))
|
logger.Error(err, "Token validation failed - user not found")
|
||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.Active {
|
if !user.Active {
|
||||||
log.LogWarn("Token validation failed - user inactive", log.F("user_id", user.ID))
|
logger.Warn("Token validation failed - user inactive")
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("Token validated successfully", log.F("user_id", user.ID))
|
logger.Info("Token validated successfully")
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,22 @@ package bookmark
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BookmarkCommands contains the command handlers for the bookmark aggregate.
|
// BookmarkCommands contains the command handlers for the bookmark aggregate.
|
||||||
type BookmarkCommands struct {
|
type BookmarkCommands struct {
|
||||||
repo domain.BookmarkRepository
|
repo domain.BookmarkRepository
|
||||||
|
analyticsSvc analytics.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBookmarkCommands creates a new BookmarkCommands handler.
|
// NewBookmarkCommands creates a new BookmarkCommands handler.
|
||||||
func NewBookmarkCommands(repo domain.BookmarkRepository) *BookmarkCommands {
|
func NewBookmarkCommands(repo domain.BookmarkRepository, analyticsSvc analytics.Service) *BookmarkCommands {
|
||||||
return &BookmarkCommands{repo: repo}
|
return &BookmarkCommands{
|
||||||
|
repo: repo,
|
||||||
|
analyticsSvc: analyticsSvc,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBookmarkInput represents the input for creating a new bookmark.
|
// CreateBookmarkInput represents the input for creating a new bookmark.
|
||||||
@ -35,6 +40,11 @@ func (c *BookmarkCommands) CreateBookmark(ctx context.Context, input CreateBookm
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.analyticsSvc != nil {
|
||||||
|
go c.analyticsSvc.IncrementWorkBookmarks(context.Background(), input.WorkID)
|
||||||
|
}
|
||||||
|
|
||||||
return bookmark, nil
|
return bookmark, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package bookmark
|
package bookmark
|
||||||
|
|
||||||
import "tercul/internal/domain"
|
import (
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
// Service is the application service for the bookmark aggregate.
|
// Service is the application service for the bookmark aggregate.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -9,9 +12,9 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new bookmark Service.
|
// NewService creates a new bookmark Service.
|
||||||
func NewService(repo domain.BookmarkRepository) *Service {
|
func NewService(repo domain.BookmarkRepository, analyticsSvc analytics.Service) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
Commands: NewBookmarkCommands(repo),
|
Commands: NewBookmarkCommands(repo, analyticsSvc),
|
||||||
Queries: NewBookmarkQueries(repo),
|
Queries: NewBookmarkQueries(repo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,24 +4,25 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentCommands contains the command handlers for the comment aggregate.
|
// CommentCommands contains the command handlers for the comment aggregate.
|
||||||
type CommentCommands struct {
|
type CommentCommands struct {
|
||||||
repo domain.CommentRepository
|
repo domain.CommentRepository
|
||||||
authzSvc *authz.Service
|
authzSvc *authz.Service
|
||||||
|
analyticsSvc analytics.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommentCommands creates a new CommentCommands handler.
|
// NewCommentCommands creates a new CommentCommands handler.
|
||||||
func NewCommentCommands(repo domain.CommentRepository, authzSvc *authz.Service) *CommentCommands {
|
func NewCommentCommands(repo domain.CommentRepository, authzSvc *authz.Service, analyticsSvc analytics.Service) *CommentCommands {
|
||||||
return &CommentCommands{
|
return &CommentCommands{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
authzSvc: authzSvc,
|
authzSvc: authzSvc,
|
||||||
|
analyticsSvc: analyticsSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +48,16 @@ func (c *CommentCommands) CreateComment(ctx context.Context, input CreateComment
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.analyticsSvc != nil {
|
||||||
|
if input.WorkID != nil {
|
||||||
|
go c.analyticsSvc.IncrementWorkComments(context.Background(), *input.WorkID)
|
||||||
|
}
|
||||||
|
if input.TranslationID != nil {
|
||||||
|
go c.analyticsSvc.IncrementTranslationComments(context.Background(), *input.TranslationID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return comment, nil
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +76,8 @@ func (c *CommentCommands) UpdateComment(ctx context.Context, input UpdateComment
|
|||||||
|
|
||||||
comment, err := c.repo.GetByID(ctx, input.ID)
|
comment, err := c.repo.GetByID(ctx, input.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||||
return nil, fmt.Errorf("%w: comment with id %d not found", domain.ErrNotFound, input.ID)
|
return nil, fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, input.ID)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -96,8 +107,8 @@ func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
|
|||||||
|
|
||||||
comment, err := c.repo.GetByID(ctx, id)
|
comment, err := c.repo.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||||
return fmt.Errorf("%w: comment with id %d not found", domain.ErrNotFound, id)
|
return fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, id)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package comment
|
package comment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
)
|
)
|
||||||
@ -12,9 +13,9 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new comment Service.
|
// NewService creates a new comment Service.
|
||||||
func NewService(repo domain.CommentRepository, authzSvc *authz.Service) *Service {
|
func NewService(repo domain.CommentRepository, authzSvc *authz.Service, analyticsSvc analytics.Service) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
Commands: NewCommentCommands(repo, authzSvc),
|
Commands: NewCommentCommands(repo, authzSvc, analyticsSvc),
|
||||||
Queries: NewCommentQueries(repo),
|
Queries: NewCommentQueries(repo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ func (c *CopyrightCommands) CreateCopyright(ctx context.Context, copyright *doma
|
|||||||
if copyright.Identificator == "" {
|
if copyright.Identificator == "" {
|
||||||
return errors.New("copyright identificator cannot be empty")
|
return errors.New("copyright identificator cannot be empty")
|
||||||
}
|
}
|
||||||
log.LogDebug("Creating copyright", log.F("name", copyright.Name))
|
log.FromContext(ctx).With("name", copyright.Name).Debug("Creating copyright")
|
||||||
return c.repo.Create(ctx, copyright)
|
return c.repo.Create(ctx, copyright)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma
|
|||||||
if copyright.Identificator == "" {
|
if copyright.Identificator == "" {
|
||||||
return errors.New("copyright identificator cannot be empty")
|
return errors.New("copyright identificator cannot be empty")
|
||||||
}
|
}
|
||||||
log.LogDebug("Updating copyright", log.F("id", copyright.ID))
|
log.FromContext(ctx).With("id", copyright.ID).Debug("Updating copyright")
|
||||||
return c.repo.Update(ctx, copyright)
|
return c.repo.Update(ctx, copyright)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error
|
|||||||
if id == 0 {
|
if id == 0 {
|
||||||
return errors.New("invalid copyright ID")
|
return errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Deleting copyright", log.F("id", id))
|
log.FromContext(ctx).With("id", id).Debug("Deleting copyright")
|
||||||
return c.repo.Delete(ctx, id)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint,
|
|||||||
if workID == 0 || copyrightID == 0 {
|
if workID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid work ID or copyright ID")
|
return errors.New("invalid work ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding copyright to work", log.F("work_id", workID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Adding copyright to work")
|
||||||
return c.repo.AddCopyrightToWork(ctx, workID, copyrightID)
|
return c.repo.AddCopyrightToWork(ctx, workID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID
|
|||||||
if workID == 0 || copyrightID == 0 {
|
if workID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid work ID or copyright ID")
|
return errors.New("invalid work ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing copyright from work", log.F("work_id", workID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Removing copyright from work")
|
||||||
return c.repo.RemoveCopyrightFromWork(ctx, workID, copyrightID)
|
return c.repo.RemoveCopyrightFromWork(ctx, workID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID u
|
|||||||
if authorID == 0 || copyrightID == 0 {
|
if authorID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid author ID or copyright ID")
|
return errors.New("invalid author ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding copyright to author", log.F("author_id", authorID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Adding copyright to author")
|
||||||
return c.repo.AddCopyrightToAuthor(ctx, authorID, copyrightID)
|
return c.repo.AddCopyrightToAuthor(ctx, authorID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, autho
|
|||||||
if authorID == 0 || copyrightID == 0 {
|
if authorID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid author ID or copyright ID")
|
return errors.New("invalid author ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing copyright from author", log.F("author_id", authorID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Removing copyright from author")
|
||||||
return c.repo.RemoveCopyrightFromAuthor(ctx, authorID, copyrightID)
|
return c.repo.RemoveCopyrightFromAuthor(ctx, authorID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint,
|
|||||||
if bookID == 0 || copyrightID == 0 {
|
if bookID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid book ID or copyright ID")
|
return errors.New("invalid book ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding copyright to book", log.F("book_id", bookID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Adding copyright to book")
|
||||||
return c.repo.AddCopyrightToBook(ctx, bookID, copyrightID)
|
return c.repo.AddCopyrightToBook(ctx, bookID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID
|
|||||||
if bookID == 0 || copyrightID == 0 {
|
if bookID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid book ID or copyright ID")
|
return errors.New("invalid book ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing copyright from book", log.F("book_id", bookID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Removing copyright from book")
|
||||||
return c.repo.RemoveCopyrightFromBook(ctx, bookID, copyrightID)
|
return c.repo.RemoveCopyrightFromBook(ctx, bookID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publish
|
|||||||
if publisherID == 0 || copyrightID == 0 {
|
if publisherID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid publisher ID or copyright ID")
|
return errors.New("invalid publisher ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding copyright to publisher", log.F("publisher_id", publisherID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Adding copyright to publisher")
|
||||||
return c.repo.AddCopyrightToPublisher(ctx, publisherID, copyrightID)
|
return c.repo.AddCopyrightToPublisher(ctx, publisherID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, pu
|
|||||||
if publisherID == 0 || copyrightID == 0 {
|
if publisherID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid publisher ID or copyright ID")
|
return errors.New("invalid publisher ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing copyright from publisher", log.F("publisher_id", publisherID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Removing copyright from publisher")
|
||||||
return c.repo.RemoveCopyrightFromPublisher(ctx, publisherID, copyrightID)
|
return c.repo.RemoveCopyrightFromPublisher(ctx, publisherID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID u
|
|||||||
if sourceID == 0 || copyrightID == 0 {
|
if sourceID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid source ID or copyright ID")
|
return errors.New("invalid source ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding copyright to source", log.F("source_id", sourceID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Adding copyright to source")
|
||||||
return c.repo.AddCopyrightToSource(ctx, sourceID, copyrightID)
|
return c.repo.AddCopyrightToSource(ctx, sourceID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourc
|
|||||||
if sourceID == 0 || copyrightID == 0 {
|
if sourceID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid source ID or copyright ID")
|
return errors.New("invalid source ID or copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing copyright from source", log.F("source_id", sourceID), log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Removing copyright from source")
|
||||||
return c.repo.RemoveCopyrightFromSource(ctx, sourceID, copyrightID)
|
return c.repo.RemoveCopyrightFromSource(ctx, sourceID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +163,6 @@ func (c *CopyrightCommands) AddTranslation(ctx context.Context, translation *dom
|
|||||||
if translation.Message == "" {
|
if translation.Message == "" {
|
||||||
return errors.New("translation message cannot be empty")
|
return errors.New("translation message cannot be empty")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding translation to copyright", log.F("copyright_id", translation.CopyrightID), log.F("language", translation.LanguageCode))
|
log.FromContext(ctx).With("copyright_id", translation.CopyrightID).With("language", translation.LanguageCode).Debug("Adding translation to copyright")
|
||||||
return c.repo.AddTranslation(ctx, translation)
|
return c.repo.AddTranslation(ctx, translation)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,13 +28,13 @@ func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*doma
|
|||||||
if id == 0 {
|
if id == 0 {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Getting copyright by ID", log.F("id", id))
|
log.FromContext(ctx).With("id", id).Debug("Getting copyright by ID")
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCopyrights retrieves all copyrights.
|
// ListCopyrights retrieves all copyrights.
|
||||||
func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyright, error) {
|
func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyright, error) {
|
||||||
log.LogDebug("Listing all copyrights")
|
log.FromContext(ctx).Debug("Listing all copyrights")
|
||||||
// Note: This might need pagination in the future.
|
// Note: This might need pagination in the future.
|
||||||
// For now, it mirrors the old service's behavior.
|
// For now, it mirrors the old service's behavior.
|
||||||
return q.repo.ListAll(ctx)
|
return q.repo.ListAll(ctx)
|
||||||
@ -42,7 +42,7 @@ func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyrig
|
|||||||
|
|
||||||
// GetCopyrightsForWork gets all copyrights for a specific work.
|
// GetCopyrightsForWork gets all copyrights for a specific work.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*domain.Copyright, error) {
|
||||||
log.LogDebug("Getting copyrights for work", log.F("work_id", workID))
|
log.FromContext(ctx).With("work_id", workID).Debug("Getting copyrights for work")
|
||||||
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -52,7 +52,7 @@ func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint
|
|||||||
|
|
||||||
// GetCopyrightsForAuthor gets all copyrights for a specific author.
|
// GetCopyrightsForAuthor gets all copyrights for a specific author.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uint) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uint) ([]*domain.Copyright, error) {
|
||||||
log.LogDebug("Getting copyrights for author", log.F("author_id", authorID))
|
log.FromContext(ctx).With("author_id", authorID).Debug("Getting copyrights for author")
|
||||||
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -62,7 +62,7 @@ func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID
|
|||||||
|
|
||||||
// GetCopyrightsForBook gets all copyrights for a specific book.
|
// GetCopyrightsForBook gets all copyrights for a specific book.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint) ([]*domain.Copyright, error) {
|
||||||
log.LogDebug("Getting copyrights for book", log.F("book_id", bookID))
|
log.FromContext(ctx).With("book_id", bookID).Debug("Getting copyrights for book")
|
||||||
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -72,7 +72,7 @@ func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint
|
|||||||
|
|
||||||
// GetCopyrightsForPublisher gets all copyrights for a specific publisher.
|
// GetCopyrightsForPublisher gets all copyrights for a specific publisher.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Copyright, error) {
|
||||||
log.LogDebug("Getting copyrights for publisher", log.F("publisher_id", publisherID))
|
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting copyrights for publisher")
|
||||||
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -82,7 +82,7 @@ func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publis
|
|||||||
|
|
||||||
// GetCopyrightsForSource gets all copyrights for a specific source.
|
// GetCopyrightsForSource gets all copyrights for a specific source.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uint) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uint) ([]*domain.Copyright, error) {
|
||||||
log.LogDebug("Getting copyrights for source", log.F("source_id", sourceID))
|
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting copyrights for source")
|
||||||
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -95,7 +95,7 @@ func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint
|
|||||||
if copyrightID == 0 {
|
if copyrightID == 0 {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Getting translations for copyright", log.F("copyright_id", copyrightID))
|
log.FromContext(ctx).With("copyright_id", copyrightID).Debug("Getting translations for copyright")
|
||||||
return q.repo.GetTranslations(ctx, copyrightID)
|
return q.repo.GetTranslations(ctx, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +107,6 @@ func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrig
|
|||||||
if languageCode == "" {
|
if languageCode == "" {
|
||||||
return nil, errors.New("language code cannot be empty")
|
return nil, errors.New("language code cannot be empty")
|
||||||
}
|
}
|
||||||
log.LogDebug("Getting translation by language for copyright", log.F("copyright_id", copyrightID), log.F("language", languageCode))
|
log.FromContext(ctx).With("copyright_id", copyrightID).With("language", languageCode).Debug("Getting translation by language for copyright")
|
||||||
return q.repo.GetTranslationByLanguage(ctx, copyrightID, languageCode)
|
return q.repo.GetTranslationByLanguage(ctx, copyrightID, languageCode)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,22 @@ package like
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LikeCommands contains the command handlers for the like aggregate.
|
// LikeCommands contains the command handlers for the like aggregate.
|
||||||
type LikeCommands struct {
|
type LikeCommands struct {
|
||||||
repo domain.LikeRepository
|
repo domain.LikeRepository
|
||||||
|
analyticsSvc analytics.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLikeCommands creates a new LikeCommands handler.
|
// NewLikeCommands creates a new LikeCommands handler.
|
||||||
func NewLikeCommands(repo domain.LikeRepository) *LikeCommands {
|
func NewLikeCommands(repo domain.LikeRepository, analyticsSvc analytics.Service) *LikeCommands {
|
||||||
return &LikeCommands{repo: repo}
|
return &LikeCommands{
|
||||||
|
repo: repo,
|
||||||
|
analyticsSvc: analyticsSvc,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLikeInput represents the input for creating a new like.
|
// CreateLikeInput represents the input for creating a new like.
|
||||||
@ -23,7 +28,7 @@ type CreateLikeInput struct {
|
|||||||
CommentID *uint
|
CommentID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLike creates a new like.
|
// CreateLike creates a new like and increments the relevant counter.
|
||||||
func (c *LikeCommands) CreateLike(ctx context.Context, input CreateLikeInput) (*domain.Like, error) {
|
func (c *LikeCommands) CreateLike(ctx context.Context, input CreateLikeInput) (*domain.Like, error) {
|
||||||
like := &domain.Like{
|
like := &domain.Like{
|
||||||
UserID: input.UserID,
|
UserID: input.UserID,
|
||||||
@ -35,6 +40,21 @@ func (c *LikeCommands) CreateLike(ctx context.Context, input CreateLikeInput) (*
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After creating the like, increment the appropriate counter.
|
||||||
|
if c.analyticsSvc != nil {
|
||||||
|
if input.WorkID != nil {
|
||||||
|
go c.analyticsSvc.IncrementWorkLikes(context.Background(), *input.WorkID)
|
||||||
|
}
|
||||||
|
if input.TranslationID != nil {
|
||||||
|
go c.analyticsSvc.IncrementTranslationLikes(context.Background(), *input.TranslationID)
|
||||||
|
}
|
||||||
|
// Assuming there's a counter for comment likes, which is a reasonable feature to add.
|
||||||
|
// if input.CommentID != nil {
|
||||||
|
// go c.analyticsSvc.IncrementCommentLikes(context.Background(), *input.CommentID)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
return like, nil
|
return like, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package like
|
package like
|
||||||
|
|
||||||
import "tercul/internal/domain"
|
import (
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
// Service is the application service for the like aggregate.
|
// Service is the application service for the like aggregate.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -9,9 +12,9 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new like Service.
|
// NewService creates a new like Service.
|
||||||
func NewService(repo domain.LikeRepository) *Service {
|
func NewService(repo domain.LikeRepository, analyticsSvc analytics.Service) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
Commands: NewLikeCommands(repo),
|
Commands: NewLikeCommands(repo, analyticsSvc),
|
||||||
Queries: NewLikeQueries(repo),
|
Queries: NewLikeQueries(repo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,3 +29,8 @@ func (q *LocalizationQueries) GetTranslations(ctx context.Context, keys []string
|
|||||||
func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) {
|
func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) {
|
||||||
return q.repo.GetAuthorBiography(ctx, authorID, language)
|
return q.repo.GetAuthorBiography(ctx, authorID, language)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWorkContent returns the content of a work in a specific language.
|
||||||
|
func (q *LocalizationQueries) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
||||||
|
return q.repo.GetWorkContent(ctx, workID, language)
|
||||||
|
}
|
||||||
@ -30,6 +30,11 @@ func (m *mockLocalizationRepository) GetAuthorBiography(ctx context.Context, aut
|
|||||||
return args.String(0), args.Error(1)
|
return args.String(0), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockLocalizationRepository) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
||||||
|
args := m.Called(ctx, workID, language)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocalizationService_GetTranslation(t *testing.T) {
|
func TestLocalizationService_GetTranslation(t *testing.T) {
|
||||||
repo := new(mockLocalizationRepository)
|
repo := new(mockLocalizationRepository)
|
||||||
service := NewService(repo)
|
service := NewService(repo)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID
|
|||||||
if workID == 0 || monetizationID == 0 {
|
if workID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid work ID or monetization ID")
|
return errors.New("invalid work ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding monetization to work", log.F("work_id", workID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Adding monetization to work")
|
||||||
return c.repo.AddMonetizationToWork(ctx, workID, monetizationID)
|
return c.repo.AddMonetizationToWork(ctx, workID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, w
|
|||||||
if workID == 0 || monetizationID == 0 {
|
if workID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid work ID or monetization ID")
|
return errors.New("invalid work ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing monetization from work", log.F("work_id", workID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Removing monetization from work")
|
||||||
return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID)
|
return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, auth
|
|||||||
if authorID == 0 || monetizationID == 0 {
|
if authorID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid author ID or monetization ID")
|
return errors.New("invalid author ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding monetization to author", log.F("author_id", authorID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Adding monetization to author")
|
||||||
return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID)
|
return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context,
|
|||||||
if authorID == 0 || monetizationID == 0 {
|
if authorID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid author ID or monetization ID")
|
return errors.New("invalid author ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing monetization from author", log.F("author_id", authorID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Removing monetization from author")
|
||||||
return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID)
|
return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID
|
|||||||
if bookID == 0 || monetizationID == 0 {
|
if bookID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid book ID or monetization ID")
|
return errors.New("invalid book ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding monetization to book", log.F("book_id", bookID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Adding monetization to book")
|
||||||
return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID)
|
return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, b
|
|||||||
if bookID == 0 || monetizationID == 0 {
|
if bookID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid book ID or monetization ID")
|
return errors.New("invalid book ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing monetization from book", log.F("book_id", bookID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Removing monetization from book")
|
||||||
return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID)
|
return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, p
|
|||||||
if publisherID == 0 || monetizationID == 0 {
|
if publisherID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid publisher ID or monetization ID")
|
return errors.New("invalid publisher ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding monetization to publisher", log.F("publisher_id", publisherID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Adding monetization to publisher")
|
||||||
return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID)
|
return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Conte
|
|||||||
if publisherID == 0 || monetizationID == 0 {
|
if publisherID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid publisher ID or monetization ID")
|
return errors.New("invalid publisher ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing monetization from publisher", log.F("publisher_id", publisherID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Removing monetization from publisher")
|
||||||
return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID)
|
return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sour
|
|||||||
if sourceID == 0 || monetizationID == 0 {
|
if sourceID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid source ID or monetization ID")
|
return errors.New("invalid source ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Adding monetization to source", log.F("source_id", sourceID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Adding monetization to source")
|
||||||
return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID)
|
return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +95,6 @@ func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context,
|
|||||||
if sourceID == 0 || monetizationID == 0 {
|
if sourceID == 0 || monetizationID == 0 {
|
||||||
return errors.New("invalid source ID or monetization ID")
|
return errors.New("invalid source ID or monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Removing monetization from source", log.F("source_id", sourceID), log.F("monetization_id", monetizationID))
|
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Removing monetization from source")
|
||||||
return c.repo.RemoveMonetizationFromSource(ctx, sourceID, monetizationID)
|
return c.repo.RemoveMonetizationFromSource(ctx, sourceID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,18 +28,18 @@ func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uint)
|
|||||||
if id == 0 {
|
if id == 0 {
|
||||||
return nil, errors.New("invalid monetization ID")
|
return nil, errors.New("invalid monetization ID")
|
||||||
}
|
}
|
||||||
log.LogDebug("Getting monetization by ID", log.F("id", id))
|
log.FromContext(ctx).With("id", id).Debug("Getting monetization by ID")
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListMonetizations retrieves all monetizations.
|
// ListMonetizations retrieves all monetizations.
|
||||||
func (q *MonetizationQueries) ListMonetizations(ctx context.Context) ([]domain.Monetization, error) {
|
func (q *MonetizationQueries) ListMonetizations(ctx context.Context) ([]domain.Monetization, error) {
|
||||||
log.LogDebug("Listing all monetizations")
|
log.FromContext(ctx).Debug("Listing all monetizations")
|
||||||
return q.repo.ListAll(ctx)
|
return q.repo.ListAll(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*domain.Monetization, error) {
|
||||||
log.LogDebug("Getting monetizations for work", log.F("work_id", workID))
|
log.FromContext(ctx).With("work_id", workID).Debug("Getting monetizations for work")
|
||||||
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -48,7 +48,7 @@ func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*domain.Monetization, error) {
|
||||||
log.LogDebug("Getting monetizations for author", log.F("author_id", authorID))
|
log.FromContext(ctx).With("author_id", authorID).Debug("Getting monetizations for author")
|
||||||
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -57,7 +57,7 @@ func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, aut
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uint) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uint) ([]*domain.Monetization, error) {
|
||||||
log.LogDebug("Getting monetizations for book", log.F("book_id", bookID))
|
log.FromContext(ctx).With("book_id", bookID).Debug("Getting monetizations for book")
|
||||||
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -66,7 +66,7 @@ func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Monetization, error) {
|
||||||
log.LogDebug("Getting monetizations for publisher", log.F("publisher_id", publisherID))
|
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting monetizations for publisher")
|
||||||
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -75,7 +75,7 @@ func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uint) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uint) ([]*domain.Monetization, error) {
|
||||||
log.LogDebug("Getting monetizations for source", log.F("source_id", sourceID))
|
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting monetizations for source")
|
||||||
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -7,6 +7,9 @@ import (
|
|||||||
"tercul/internal/domain/work"
|
"tercul/internal/domain/work"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
"tercul/internal/platform/search"
|
"tercul/internal/platform/search"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IndexService pushes localized snapshots into Weaviate for search
|
// IndexService pushes localized snapshots into Weaviate for search
|
||||||
@ -17,29 +20,38 @@ type IndexService interface {
|
|||||||
type indexService struct {
|
type indexService struct {
|
||||||
localization *localization.Service
|
localization *localization.Service
|
||||||
weaviate search.WeaviateWrapper
|
weaviate search.WeaviateWrapper
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndexService(localization *localization.Service, weaviate search.WeaviateWrapper) IndexService {
|
func NewIndexService(localization *localization.Service, weaviate search.WeaviateWrapper) IndexService {
|
||||||
return &indexService{localization: localization, weaviate: weaviate}
|
return &indexService{
|
||||||
|
localization: localization,
|
||||||
|
weaviate: weaviate,
|
||||||
|
tracer: otel.Tracer("search.service"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *indexService) IndexWork(ctx context.Context, work work.Work) error {
|
func (s *indexService) IndexWork(ctx context.Context, work work.Work) error {
|
||||||
log.LogDebug("Indexing work", log.F("work_id", work.ID))
|
ctx, span := s.tracer.Start(ctx, "IndexWork")
|
||||||
// TODO: Get content from translation service
|
defer span.End()
|
||||||
content := ""
|
logger := log.FromContext(ctx).With("work_id", work.ID)
|
||||||
// Choose best content snapshot for indexing
|
logger.Debug("Indexing work")
|
||||||
// content, err := s.localization.GetWorkContent(ctx, work.ID, work.Language)
|
|
||||||
// if err != nil {
|
|
||||||
// log.LogError("Failed to get work content for indexing", log.F("work_id", work.ID), log.F("error", err))
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
err := s.weaviate.IndexWork(ctx, &work, content)
|
// Get content from translation service
|
||||||
|
content, err := s.localization.Queries.GetWorkContent(ctx, work.ID, work.Language)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to index work in Weaviate", log.F("work_id", work.ID), log.F("error", err))
|
logger.Error(err, "Failed to get work content for indexing")
|
||||||
|
// We can choose to index without content or return an error.
|
||||||
|
// For now, we'll log the error and continue indexing with empty content.
|
||||||
|
content = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.weaviate.IndexWork(ctx, &work, content)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "Failed to index work in Weaviate")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.LogInfo("Successfully indexed work", log.F("work_id", work.ID))
|
logger.Info("Successfully indexed work")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,11 @@ func (m *mockLocalizationRepository) GetAuthorBiography(ctx context.Context, aut
|
|||||||
return args.String(0), args.Error(1)
|
return args.String(0), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockLocalizationRepository) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
||||||
|
args := m.Called(ctx, workID, language)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
type mockWeaviateWrapper struct {
|
type mockWeaviateWrapper struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
@ -49,20 +54,24 @@ func TestIndexService_IndexWork(t *testing.T) {
|
|||||||
service := NewIndexService(localizationService, weaviateWrapper)
|
service := NewIndexService(localizationService, weaviateWrapper)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
work := work.Work{
|
testWork := work.Work{
|
||||||
TranslatableModel: domain.TranslatableModel{
|
TranslatableModel: domain.TranslatableModel{
|
||||||
BaseModel: domain.BaseModel{ID: 1},
|
BaseModel: domain.BaseModel{ID: 1},
|
||||||
Language: "en",
|
Language: "en",
|
||||||
},
|
},
|
||||||
Title: "Test Work",
|
Title: "Test Work",
|
||||||
}
|
}
|
||||||
|
testContent := "This is the test content for the work."
|
||||||
|
|
||||||
// localizationRepo.On("GetTranslation", ctx, "work:1:content", "en").Return("Test content", nil)
|
// Expect a call to get the work's content.
|
||||||
weaviateWrapper.On("IndexWork", ctx, &work, "").Return(nil)
|
localizationRepo.On("GetWorkContent", mock.Anything, testWork.ID, testWork.Language).Return(testContent, nil)
|
||||||
|
|
||||||
err := service.IndexWork(ctx, work)
|
// Expect a call to the Weaviate wrapper with the fetched content.
|
||||||
|
weaviateWrapper.On("IndexWork", mock.Anything, &testWork, testContent).Return(nil)
|
||||||
|
|
||||||
|
err := service.IndexWork(ctx, testWork)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// localizationRepo.AssertExpectations(t)
|
localizationRepo.AssertExpectations(t)
|
||||||
weaviateWrapper.AssertExpectations(t)
|
weaviateWrapper.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,13 +8,15 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TranslationCommands contains the command handlers for the translation aggregate.
|
// TranslationCommands contains the command handlers for the translation aggregate.
|
||||||
type TranslationCommands struct {
|
type TranslationCommands struct {
|
||||||
repo domain.TranslationRepository
|
repo domain.TranslationRepository
|
||||||
authzSvc *authz.Service
|
authzSvc *authz.Service
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTranslationCommands creates a new TranslationCommands handler.
|
// NewTranslationCommands creates a new TranslationCommands handler.
|
||||||
@ -22,6 +24,7 @@ func NewTranslationCommands(repo domain.TranslationRepository, authzSvc *authz.S
|
|||||||
return &TranslationCommands{
|
return &TranslationCommands{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
authzSvc: authzSvc,
|
authzSvc: authzSvc,
|
||||||
|
tracer: otel.Tracer("translation.commands"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +43,8 @@ type CreateTranslationInput struct {
|
|||||||
|
|
||||||
// CreateTranslation creates a new translation.
|
// CreateTranslation creates a new translation.
|
||||||
func (c *TranslationCommands) CreateTranslation(ctx context.Context, input CreateTranslationInput) (*domain.Translation, error) {
|
func (c *TranslationCommands) CreateTranslation(ctx context.Context, input CreateTranslationInput) (*domain.Translation, error) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "CreateTranslation")
|
||||||
|
defer span.End()
|
||||||
translation := &domain.Translation{
|
translation := &domain.Translation{
|
||||||
Title: input.Title,
|
Title: input.Title,
|
||||||
Content: input.Content,
|
Content: input.Content,
|
||||||
@ -70,6 +75,8 @@ type UpdateTranslationInput struct {
|
|||||||
|
|
||||||
// UpdateTranslation updates an existing translation.
|
// UpdateTranslation updates an existing translation.
|
||||||
func (c *TranslationCommands) UpdateTranslation(ctx context.Context, input UpdateTranslationInput) (*domain.Translation, error) {
|
func (c *TranslationCommands) UpdateTranslation(ctx context.Context, input UpdateTranslationInput) (*domain.Translation, error) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "UpdateTranslation")
|
||||||
|
defer span.End()
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, domain.ErrUnauthorized
|
return nil, domain.ErrUnauthorized
|
||||||
@ -85,8 +92,8 @@ func (c *TranslationCommands) UpdateTranslation(ctx context.Context, input Updat
|
|||||||
|
|
||||||
translation, err := c.repo.GetByID(ctx, input.ID)
|
translation, err := c.repo.GetByID(ctx, input.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||||
return nil, fmt.Errorf("%w: translation with id %d not found", domain.ErrNotFound, input.ID)
|
return nil, fmt.Errorf("%w: translation with id %d not found", domain.ErrEntityNotFound, input.ID)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -105,6 +112,8 @@ func (c *TranslationCommands) UpdateTranslation(ctx context.Context, input Updat
|
|||||||
|
|
||||||
// DeleteTranslation deletes a translation by ID.
|
// DeleteTranslation deletes a translation by ID.
|
||||||
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error {
|
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "DeleteTranslation")
|
||||||
|
defer span.End()
|
||||||
can, err := c.authzSvc.CanDeleteTranslation(ctx)
|
can, err := c.authzSvc.CanDeleteTranslation(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -3,44 +3,63 @@ package translation
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TranslationQueries contains the query handlers for the translation aggregate.
|
// TranslationQueries contains the query handlers for the translation aggregate.
|
||||||
type TranslationQueries struct {
|
type TranslationQueries struct {
|
||||||
repo domain.TranslationRepository
|
repo domain.TranslationRepository
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTranslationQueries creates a new TranslationQueries handler.
|
// NewTranslationQueries creates a new TranslationQueries handler.
|
||||||
func NewTranslationQueries(repo domain.TranslationRepository) *TranslationQueries {
|
func NewTranslationQueries(repo domain.TranslationRepository) *TranslationQueries {
|
||||||
return &TranslationQueries{repo: repo}
|
return &TranslationQueries{
|
||||||
|
repo: repo,
|
||||||
|
tracer: otel.Tracer("translation.queries"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translation returns a translation by ID.
|
// Translation returns a translation by ID.
|
||||||
func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*domain.Translation, error) {
|
func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*domain.Translation, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "Translation")
|
||||||
|
defer span.End()
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByWorkID returns all translations for a work.
|
// TranslationsByWorkID returns all translations for a work.
|
||||||
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "TranslationsByWorkID")
|
||||||
|
defer span.End()
|
||||||
return q.repo.ListByWorkID(ctx, workID)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByEntity returns all translations for an entity.
|
// TranslationsByEntity returns all translations for an entity.
|
||||||
func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "TranslationsByEntity")
|
||||||
|
defer span.End()
|
||||||
return q.repo.ListByEntity(ctx, entityType, entityID)
|
return q.repo.ListByEntity(ctx, entityType, entityID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByTranslatorID returns all translations for a translator.
|
// TranslationsByTranslatorID returns all translations for a translator.
|
||||||
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "TranslationsByTranslatorID")
|
||||||
|
defer span.End()
|
||||||
return q.repo.ListByTranslatorID(ctx, translatorID)
|
return q.repo.ListByTranslatorID(ctx, translatorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByStatus returns all translations for a status.
|
// TranslationsByStatus returns all translations for a status.
|
||||||
func (q *TranslationQueries) TranslationsByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
func (q *TranslationQueries) TranslationsByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "TranslationsByStatus")
|
||||||
|
defer span.End()
|
||||||
return q.repo.ListByStatus(ctx, status)
|
return q.repo.ListByStatus(ctx, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translations returns all translations.
|
// Translations returns all translations.
|
||||||
func (q *TranslationQueries) Translations(ctx context.Context) ([]domain.Translation, error) {
|
func (q *TranslationQueries) Translations(ctx context.Context) ([]domain.Translation, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "Translations")
|
||||||
|
defer span.End()
|
||||||
return q.repo.ListAll(ctx)
|
return q.repo.ListAll(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import (
|
|||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserCommands contains the command handlers for the user aggregate.
|
// UserCommands contains the command handlers for the user aggregate.
|
||||||
@ -88,8 +86,8 @@ func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*
|
|||||||
|
|
||||||
user, err := c.repo.GetByID(ctx, input.ID)
|
user, err := c.repo.GetByID(ctx, input.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||||
return nil, fmt.Errorf("%w: user with id %d not found", domain.ErrNotFound, input.ID)
|
return nil, fmt.Errorf("%w: user with id %d not found", domain.ErrEntityNotFound, input.ID)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import (
|
|||||||
"tercul/internal/domain/work"
|
"tercul/internal/domain/work"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ type WorkCommands struct {
|
|||||||
repo work.WorkRepository
|
repo work.WorkRepository
|
||||||
searchClient search.SearchClient
|
searchClient search.SearchClient
|
||||||
authzSvc *authz.Service
|
authzSvc *authz.Service
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkCommands creates a new WorkCommands handler.
|
// NewWorkCommands creates a new WorkCommands handler.
|
||||||
@ -26,11 +29,14 @@ func NewWorkCommands(repo work.WorkRepository, searchClient search.SearchClient,
|
|||||||
repo: repo,
|
repo: repo,
|
||||||
searchClient: searchClient,
|
searchClient: searchClient,
|
||||||
authzSvc: authzSvc,
|
authzSvc: authzSvc,
|
||||||
|
tracer: otel.Tracer("work.commands"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateWork creates a new work.
|
// CreateWork creates a new work.
|
||||||
func (c *WorkCommands) CreateWork(ctx context.Context, work *work.Work) (*work.Work, error) {
|
func (c *WorkCommands) CreateWork(ctx context.Context, work *work.Work) (*work.Work, error) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "CreateWork")
|
||||||
|
defer span.End()
|
||||||
if work == nil {
|
if work == nil {
|
||||||
return nil, errors.New("work cannot be nil")
|
return nil, errors.New("work cannot be nil")
|
||||||
}
|
}
|
||||||
@ -54,6 +60,8 @@ func (c *WorkCommands) CreateWork(ctx context.Context, work *work.Work) (*work.W
|
|||||||
|
|
||||||
// UpdateWork updates an existing work after performing an authorization check.
|
// UpdateWork updates an existing work after performing an authorization check.
|
||||||
func (c *WorkCommands) UpdateWork(ctx context.Context, work *work.Work) error {
|
func (c *WorkCommands) UpdateWork(ctx context.Context, work *work.Work) error {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "UpdateWork")
|
||||||
|
defer span.End()
|
||||||
if work == nil {
|
if work == nil {
|
||||||
return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
|
return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
@ -68,8 +76,8 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *work.Work) error {
|
|||||||
|
|
||||||
existingWork, err := c.repo.GetByID(ctx, work.ID)
|
existingWork, err := c.repo.GetByID(ctx, work.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||||
return fmt.Errorf("%w: work with id %d not found", domain.ErrNotFound, work.ID)
|
return fmt.Errorf("%w: work with id %d not found", domain.ErrEntityNotFound, work.ID)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to get work for authorization: %w", err)
|
return fmt.Errorf("failed to get work for authorization: %w", err)
|
||||||
}
|
}
|
||||||
@ -99,6 +107,8 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *work.Work) error {
|
|||||||
|
|
||||||
// DeleteWork deletes a work by ID after performing an authorization check.
|
// DeleteWork deletes a work by ID after performing an authorization check.
|
||||||
func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "DeleteWork")
|
||||||
|
defer span.End()
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
@ -110,8 +120,8 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
|||||||
|
|
||||||
existingWork, err := c.repo.GetByID(ctx, id)
|
existingWork, err := c.repo.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||||
return fmt.Errorf("%w: work with id %d not found", domain.ErrNotFound, id)
|
return fmt.Errorf("%w: work with id %d not found", domain.ErrEntityNotFound, id)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to get work for authorization: %w", err)
|
return fmt.Errorf("failed to get work for authorization: %w", err)
|
||||||
}
|
}
|
||||||
@ -132,12 +142,16 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
|||||||
|
|
||||||
// AnalyzeWork performs linguistic analysis on a work.
|
// AnalyzeWork performs linguistic analysis on a work.
|
||||||
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
|
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "AnalyzeWork")
|
||||||
|
defer span.End()
|
||||||
// TODO: implement this
|
// TODO: implement this
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeWork merges two works, moving all associations from the source to the target and deleting the source.
|
// MergeWork merges two works, moving all associations from the source to the target and deleting the source.
|
||||||
func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) error {
|
func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) error {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "MergeWork")
|
||||||
|
defer span.End()
|
||||||
if sourceID == targetID {
|
if sourceID == targetID {
|
||||||
return fmt.Errorf("%w: source and target work IDs cannot be the same", domain.ErrValidation)
|
return fmt.Errorf("%w: source and target work IDs cannot be the same", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/work"
|
"tercul/internal/domain/work"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WorkAnalytics contains analytics data for a work
|
// WorkAnalytics contains analytics data for a work
|
||||||
@ -31,18 +34,22 @@ type TranslationAnalytics struct {
|
|||||||
|
|
||||||
// WorkQueries contains the query handlers for the work aggregate.
|
// WorkQueries contains the query handlers for the work aggregate.
|
||||||
type WorkQueries struct {
|
type WorkQueries struct {
|
||||||
repo work.WorkRepository
|
repo work.WorkRepository
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkQueries creates a new WorkQueries handler.
|
// NewWorkQueries creates a new WorkQueries handler.
|
||||||
func NewWorkQueries(repo work.WorkRepository) *WorkQueries {
|
func NewWorkQueries(repo work.WorkRepository) *WorkQueries {
|
||||||
return &WorkQueries{
|
return &WorkQueries{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
tracer: otel.Tracer("work.queries"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWorkByID retrieves a work by ID.
|
// GetWorkByID retrieves a work by ID.
|
||||||
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*work.Work, error) {
|
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*work.Work, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "GetWorkByID")
|
||||||
|
defer span.End()
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return nil, errors.New("invalid work ID")
|
return nil, errors.New("invalid work ID")
|
||||||
}
|
}
|
||||||
@ -51,11 +58,15 @@ func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*work.Work, err
|
|||||||
|
|
||||||
// ListWorks returns a paginated list of works.
|
// ListWorks returns a paginated list of works.
|
||||||
func (q *WorkQueries) ListWorks(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
func (q *WorkQueries) ListWorks(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "ListWorks")
|
||||||
|
defer span.End()
|
||||||
return q.repo.List(ctx, page, pageSize)
|
return q.repo.List(ctx, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWorkWithTranslations retrieves a work with its translations.
|
// GetWorkWithTranslations retrieves a work with its translations.
|
||||||
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
|
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "GetWorkWithTranslations")
|
||||||
|
defer span.End()
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return nil, errors.New("invalid work ID")
|
return nil, errors.New("invalid work ID")
|
||||||
}
|
}
|
||||||
@ -64,6 +75,8 @@ func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*wo
|
|||||||
|
|
||||||
// FindWorksByTitle finds works by title.
|
// FindWorksByTitle finds works by title.
|
||||||
func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]work.Work, error) {
|
func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]work.Work, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "FindWorksByTitle")
|
||||||
|
defer span.End()
|
||||||
if title == "" {
|
if title == "" {
|
||||||
return nil, errors.New("title cannot be empty")
|
return nil, errors.New("title cannot be empty")
|
||||||
}
|
}
|
||||||
@ -72,6 +85,8 @@ func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]wor
|
|||||||
|
|
||||||
// FindWorksByAuthor finds works by author ID.
|
// FindWorksByAuthor finds works by author ID.
|
||||||
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
|
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "FindWorksByAuthor")
|
||||||
|
defer span.End()
|
||||||
if authorID == 0 {
|
if authorID == 0 {
|
||||||
return nil, errors.New("invalid author ID")
|
return nil, errors.New("invalid author ID")
|
||||||
}
|
}
|
||||||
@ -80,6 +95,8 @@ func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]w
|
|||||||
|
|
||||||
// FindWorksByCategory finds works by category ID.
|
// FindWorksByCategory finds works by category ID.
|
||||||
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
|
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "FindWorksByCategory")
|
||||||
|
defer span.End()
|
||||||
if categoryID == 0 {
|
if categoryID == 0 {
|
||||||
return nil, errors.New("invalid category ID")
|
return nil, errors.New("invalid category ID")
|
||||||
}
|
}
|
||||||
@ -88,6 +105,8 @@ func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint)
|
|||||||
|
|
||||||
// FindWorksByLanguage finds works by language.
|
// FindWorksByLanguage finds works by language.
|
||||||
func (q *WorkQueries) FindWorksByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
func (q *WorkQueries) FindWorksByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
||||||
|
ctx, span := q.tracer.Start(ctx, "FindWorksByLanguage")
|
||||||
|
defer span.End()
|
||||||
if language == "" {
|
if language == "" {
|
||||||
return nil, errors.New("language cannot be empty")
|
return nil, errors.New("language cannot be empty")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,15 +8,21 @@ import (
|
|||||||
"tercul/internal/domain/work"
|
"tercul/internal/domain/work"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type analyticsRepository struct {
|
type analyticsRepository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAnalyticsRepository(db *gorm.DB) analytics.Repository {
|
func NewAnalyticsRepository(db *gorm.DB) analytics.Repository {
|
||||||
return &analyticsRepository{db: db}
|
return &analyticsRepository{
|
||||||
|
db: db,
|
||||||
|
tracer: otel.Tracer("analytics.repository"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowedWorkCounterFields = map[string]bool{
|
var allowedWorkCounterFields = map[string]bool{
|
||||||
@ -36,6 +42,8 @@ var allowedTranslationCounterFields = map[string]bool{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error {
|
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "IncrementWorkCounter")
|
||||||
|
defer span.End()
|
||||||
if !allowedWorkCounterFields[field] {
|
if !allowedWorkCounterFields[field] {
|
||||||
return fmt.Errorf("invalid work counter field: %s", field)
|
return fmt.Errorf("invalid work counter field: %s", field)
|
||||||
}
|
}
|
||||||
@ -59,6 +67,8 @@ func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID u
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
|
func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetTrendingWorks")
|
||||||
|
defer span.End()
|
||||||
var trendingWorks []*domain.Trending
|
var trendingWorks []*domain.Trending
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Where("entity_type = ? AND time_period = ?", "Work", timePeriod).
|
Where("entity_type = ? AND time_period = ?", "Work", timePeriod).
|
||||||
@ -101,6 +111,8 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error {
|
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "IncrementTranslationCounter")
|
||||||
|
defer span.End()
|
||||||
if !allowedTranslationCounterFields[field] {
|
if !allowedTranslationCounterFields[field] {
|
||||||
return fmt.Errorf("invalid translation counter field: %s", field)
|
return fmt.Errorf("invalid translation counter field: %s", field)
|
||||||
}
|
}
|
||||||
@ -121,36 +133,50 @@ func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats work.WorkStats) error {
|
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats work.WorkStats) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "UpdateWorkStats")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Model(&work.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
|
return r.db.WithContext(ctx).Model(&work.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error {
|
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "UpdateTranslationStats")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
|
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
|
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetOrCreateWorkStats")
|
||||||
|
defer span.End()
|
||||||
var stats work.WorkStats
|
var stats work.WorkStats
|
||||||
err := r.db.WithContext(ctx).Where(work.WorkStats{WorkID: workID}).FirstOrCreate(&stats).Error
|
err := r.db.WithContext(ctx).Where(work.WorkStats{WorkID: workID}).FirstOrCreate(&stats).Error
|
||||||
return &stats, err
|
return &stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetOrCreateTranslationStats")
|
||||||
|
defer span.End()
|
||||||
var stats domain.TranslationStats
|
var stats domain.TranslationStats
|
||||||
err := r.db.WithContext(ctx).Where(domain.TranslationStats{TranslationID: translationID}).FirstOrCreate(&stats).Error
|
err := r.db.WithContext(ctx).Where(domain.TranslationStats{TranslationID: translationID}).FirstOrCreate(&stats).Error
|
||||||
return &stats, err
|
return &stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error) {
|
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetOrCreateUserEngagement")
|
||||||
|
defer span.End()
|
||||||
var engagement domain.UserEngagement
|
var engagement domain.UserEngagement
|
||||||
err := r.db.WithContext(ctx).Where(domain.UserEngagement{UserID: userID, Date: date}).FirstOrCreate(&engagement).Error
|
err := r.db.WithContext(ctx).Where(domain.UserEngagement{UserID: userID, Date: date}).FirstOrCreate(&engagement).Error
|
||||||
return &engagement, err
|
return &engagement, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error {
|
func (r *analyticsRepository) UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "UpdateUserEngagement")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Save(userEngagement).Error
|
return r.db.WithContext(ctx).Save(userEngagement).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error {
|
func (r *analyticsRepository) UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "UpdateTrendingWorks")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Clear old trending data for this time period
|
// Clear old trending data for this time period
|
||||||
if err := tx.Where("time_period = ?", timePeriod).Delete(&domain.Trending{}).Error; err != nil {
|
if err := tx.Where("time_period = ?", timePeriod).Delete(&domain.Trending{}).Error; err != nil {
|
||||||
|
|||||||
@ -5,18 +5,26 @@ import (
|
|||||||
"tercul/internal/domain/auth"
|
"tercul/internal/domain/auth"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authRepository struct {
|
type authRepository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthRepository(db *gorm.DB) auth.AuthRepository {
|
func NewAuthRepository(db *gorm.DB) auth.AuthRepository {
|
||||||
return &authRepository{db: db}
|
return &authRepository{
|
||||||
|
db: db,
|
||||||
|
tracer: otel.Tracer("auth.repository"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *authRepository) StoreToken(ctx context.Context, userID uint, token string, expiresAt time.Time) error {
|
func (r *authRepository) StoreToken(ctx context.Context, userID uint, token string, expiresAt time.Time) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "StoreToken")
|
||||||
|
defer span.End()
|
||||||
session := &auth.UserSession{
|
session := &auth.UserSession{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Token: token,
|
Token: token,
|
||||||
@ -26,5 +34,7 @@ func (r *authRepository) StoreToken(ctx context.Context, userID uint, token stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *authRepository) DeleteToken(ctx context.Context, token string) error {
|
func (r *authRepository) DeleteToken(ctx context.Context, token string) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "DeleteToken")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Where("token = ?", token).Delete(&auth.UserSession{}).Error
|
return r.db.WithContext(ctx).Where("token = ?", token).Delete(&auth.UserSession{}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authorRepository struct {
|
type authorRepository struct {
|
||||||
domain.BaseRepository[domain.Author]
|
domain.BaseRepository[domain.Author]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthorRepository creates a new AuthorRepository.
|
// NewAuthorRepository creates a new AuthorRepository.
|
||||||
@ -17,11 +20,14 @@ func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository {
|
|||||||
return &authorRepository{
|
return &authorRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Author](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Author](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("author.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByWorkID finds authors by work ID
|
// ListByWorkID finds authors by work ID
|
||||||
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var authors []domain.Author
|
var authors []domain.Author
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
|
||||||
Where("work_authors.work_id = ?", workID).
|
Where("work_authors.work_id = ?", workID).
|
||||||
@ -33,6 +39,8 @@ func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]dom
|
|||||||
|
|
||||||
// GetWithTranslations finds an author by ID and preloads their translations.
|
// GetWithTranslations finds an author by ID and preloads their translations.
|
||||||
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetWithTranslations")
|
||||||
|
defer span.End()
|
||||||
var author domain.Author
|
var author domain.Author
|
||||||
if err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error; err != nil {
|
if err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -42,6 +50,8 @@ func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*d
|
|||||||
|
|
||||||
// ListByBookID finds authors by book ID
|
// ListByBookID finds authors by book ID
|
||||||
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByBookID")
|
||||||
|
defer span.End()
|
||||||
var authors []domain.Author
|
var authors []domain.Author
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
|
||||||
Where("book_authors.book_id = ?", bookID).
|
Where("book_authors.book_id = ?", bookID).
|
||||||
@ -53,6 +63,8 @@ func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]dom
|
|||||||
|
|
||||||
// ListByCountryID finds authors by country ID
|
// ListByCountryID finds authors by country ID
|
||||||
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByCountryID")
|
||||||
|
defer span.End()
|
||||||
var authors []domain.Author
|
var authors []domain.Author
|
||||||
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import (
|
|||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,12 +26,16 @@ var (
|
|||||||
|
|
||||||
// BaseRepositoryImpl provides a default implementation of BaseRepository using GORM
|
// BaseRepositoryImpl provides a default implementation of BaseRepository using GORM
|
||||||
type BaseRepositoryImpl[T any] struct {
|
type BaseRepositoryImpl[T any] struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBaseRepositoryImpl creates a new BaseRepositoryImpl
|
// NewBaseRepositoryImpl creates a new BaseRepositoryImpl
|
||||||
func NewBaseRepositoryImpl[T any](db *gorm.DB) domain.BaseRepository[T] {
|
func NewBaseRepositoryImpl[T any](db *gorm.DB) domain.BaseRepository[T] {
|
||||||
return &BaseRepositoryImpl[T]{db: db}
|
return &BaseRepositoryImpl[T]{
|
||||||
|
db: db,
|
||||||
|
tracer: otel.Tracer("base.repository"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateContext ensures context is not nil
|
// validateContext ensures context is not nil
|
||||||
@ -113,6 +119,8 @@ func (r *BaseRepositoryImpl[T]) Create(ctx context.Context, entity *T) error {
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "Create")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateEntity(entity); err != nil {
|
if err := r.validateEntity(entity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -122,14 +130,11 @@ func (r *BaseRepositoryImpl[T]) Create(ctx context.Context, entity *T) error {
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to create entity",
|
log.Error(err, "Failed to create entity")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity created successfully",
|
log.Debug(fmt.Sprintf("Entity created successfully in %s", duration))
|
||||||
log.F("duration", duration))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +143,8 @@ func (r *BaseRepositoryImpl[T]) CreateInTx(ctx context.Context, tx *gorm.DB, ent
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "CreateInTx")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateEntity(entity); err != nil {
|
if err := r.validateEntity(entity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -150,14 +157,11 @@ func (r *BaseRepositoryImpl[T]) CreateInTx(ctx context.Context, tx *gorm.DB, ent
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to create entity in transaction",
|
log.Error(err, "Failed to create entity in transaction")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity created successfully in transaction",
|
log.Debug(fmt.Sprintf("Entity created successfully in transaction in %s", duration))
|
||||||
log.F("duration", duration))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,6 +170,8 @@ func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByID")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateID(id); err != nil {
|
if err := r.validateID(id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -177,21 +183,14 @@ func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.LogDebug("Entity not found",
|
log.Debug(fmt.Sprintf("Entity with id %d not found in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return nil, ErrEntityNotFound
|
return nil, ErrEntityNotFound
|
||||||
}
|
}
|
||||||
log.LogError("Failed to get entity by ID",
|
log.Error(err, fmt.Sprintf("Failed to get entity by ID %d", id))
|
||||||
log.F("id", id),
|
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity retrieved successfully",
|
log.Debug(fmt.Sprintf("Entity with id %d retrieved successfully in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return &entity, nil
|
return &entity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,6 +199,8 @@ func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uint,
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByIDWithOptions")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateID(id); err != nil {
|
if err := r.validateID(id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -212,21 +213,14 @@ func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uint,
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.LogDebug("Entity not found with options",
|
log.Debug(fmt.Sprintf("Entity with id %d not found with options in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return nil, ErrEntityNotFound
|
return nil, ErrEntityNotFound
|
||||||
}
|
}
|
||||||
log.LogError("Failed to get entity by ID with options",
|
log.Error(err, fmt.Sprintf("Failed to get entity by ID %d with options", id))
|
||||||
log.F("id", id),
|
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity retrieved successfully with options",
|
log.Debug(fmt.Sprintf("Entity with id %d retrieved successfully with options in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return &entity, nil
|
return &entity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +229,8 @@ func (r *BaseRepositoryImpl[T]) Update(ctx context.Context, entity *T) error {
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "Update")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateEntity(entity); err != nil {
|
if err := r.validateEntity(entity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -244,14 +240,11 @@ func (r *BaseRepositoryImpl[T]) Update(ctx context.Context, entity *T) error {
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to update entity",
|
log.Error(err, "Failed to update entity")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity updated successfully",
|
log.Debug(fmt.Sprintf("Entity updated successfully in %s", duration))
|
||||||
log.F("duration", duration))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,6 +253,8 @@ func (r *BaseRepositoryImpl[T]) UpdateInTx(ctx context.Context, tx *gorm.DB, ent
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "UpdateInTx")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateEntity(entity); err != nil {
|
if err := r.validateEntity(entity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -272,14 +267,11 @@ func (r *BaseRepositoryImpl[T]) UpdateInTx(ctx context.Context, tx *gorm.DB, ent
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to update entity in transaction",
|
log.Error(err, "Failed to update entity in transaction")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity updated successfully in transaction",
|
log.Debug(fmt.Sprintf("Entity updated successfully in transaction in %s", duration))
|
||||||
log.F("duration", duration))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,6 +280,8 @@ func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uint) error {
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "Delete")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateID(id); err != nil {
|
if err := r.validateID(id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -298,24 +292,16 @@ func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uint) error {
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.LogError("Failed to delete entity",
|
log.Error(result.Error, fmt.Sprintf("Failed to delete entity with id %d", id))
|
||||||
log.F("id", id),
|
|
||||||
log.F("error", result.Error),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, result.Error)
|
return fmt.Errorf("%w: %v", ErrDatabaseOperation, result.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.RowsAffected == 0 {
|
if result.RowsAffected == 0 {
|
||||||
log.LogDebug("No entity found to delete",
|
log.Debug(fmt.Sprintf("No entity with id %d found to delete in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return ErrEntityNotFound
|
return ErrEntityNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity deleted successfully",
|
log.Debug(fmt.Sprintf("Entity with id %d deleted successfully in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("rowsAffected", result.RowsAffected),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,6 +310,8 @@ func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "DeleteInTx")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateID(id); err != nil {
|
if err := r.validateID(id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -337,24 +325,16 @@ func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.LogError("Failed to delete entity in transaction",
|
log.Error(result.Error, fmt.Sprintf("Failed to delete entity with id %d in transaction", id))
|
||||||
log.F("id", id),
|
|
||||||
log.F("error", result.Error),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, result.Error)
|
return fmt.Errorf("%w: %v", ErrDatabaseOperation, result.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.RowsAffected == 0 {
|
if result.RowsAffected == 0 {
|
||||||
log.LogDebug("No entity found to delete in transaction",
|
log.Debug(fmt.Sprintf("No entity with id %d found to delete in transaction in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return ErrEntityNotFound
|
return ErrEntityNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Entity deleted successfully in transaction",
|
log.Debug(fmt.Sprintf("Entity with id %d deleted successfully in transaction in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("rowsAffected", result.RowsAffected),
|
|
||||||
log.F("duration", duration))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,6 +343,8 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "List")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
page, pageSize, err := r.validatePagination(page, pageSize)
|
page, pageSize, err := r.validatePagination(page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -375,9 +357,7 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
|||||||
|
|
||||||
// Get total count
|
// Get total count
|
||||||
if err := r.db.WithContext(ctx).Model(new(T)).Count(&totalCount).Error; err != nil {
|
if err := r.db.WithContext(ctx).Model(new(T)).Count(&totalCount).Error; err != nil {
|
||||||
log.LogError("Failed to count entities",
|
log.Error(err, "Failed to count entities")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,11 +366,7 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
|||||||
|
|
||||||
// Get paginated data
|
// Get paginated data
|
||||||
if err := r.db.WithContext(ctx).Offset(offset).Limit(pageSize).Find(&entities).Error; err != nil {
|
if err := r.db.WithContext(ctx).Offset(offset).Limit(pageSize).Find(&entities).Error; err != nil {
|
||||||
log.LogError("Failed to get paginated entities",
|
log.Error(err, "Failed to get paginated entities")
|
||||||
log.F("page", page),
|
|
||||||
log.F("pageSize", pageSize),
|
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,14 +381,7 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
|||||||
hasNext := page < totalPages
|
hasNext := page < totalPages
|
||||||
hasPrev := page > 1
|
hasPrev := page > 1
|
||||||
|
|
||||||
log.LogDebug("Paginated entities retrieved successfully",
|
log.Debug(fmt.Sprintf("Paginated entities retrieved successfully in %s", duration))
|
||||||
log.F("page", page),
|
|
||||||
log.F("pageSize", pageSize),
|
|
||||||
log.F("totalCount", totalCount),
|
|
||||||
log.F("totalPages", totalPages),
|
|
||||||
log.F("hasNext", hasNext),
|
|
||||||
log.F("hasPrev", hasPrev),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return &domain.PaginatedResult[T]{
|
return &domain.PaginatedResult[T]{
|
||||||
Items: entities,
|
Items: entities,
|
||||||
@ -430,22 +399,20 @@ func (r *BaseRepositoryImpl[T]) ListWithOptions(ctx context.Context, options *do
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListWithOptions")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var entities []T
|
var entities []T
|
||||||
query := r.buildQuery(r.db.WithContext(ctx), options)
|
query := r.buildQuery(r.db.WithContext(ctx), options)
|
||||||
|
|
||||||
if err := query.Find(&entities).Error; err != nil {
|
if err := query.Find(&entities).Error; err != nil {
|
||||||
log.LogError("Failed to get entities with options",
|
log.Error(err, "Failed to get entities with options")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
log.LogDebug("Entities retrieved successfully with options",
|
log.Debug(fmt.Sprintf("Entities retrieved successfully with options in %s", duration))
|
||||||
log.F("count", len(entities)),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return entities, nil
|
return entities, nil
|
||||||
}
|
}
|
||||||
@ -455,20 +422,18 @@ func (r *BaseRepositoryImpl[T]) ListAll(ctx context.Context) ([]T, error) {
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListAll")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var entities []T
|
var entities []T
|
||||||
if err := r.db.WithContext(ctx).Find(&entities).Error; err != nil {
|
if err := r.db.WithContext(ctx).Find(&entities).Error; err != nil {
|
||||||
log.LogError("Failed to get all entities",
|
log.Error(err, "Failed to get all entities")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
log.LogDebug("All entities retrieved successfully",
|
log.Debug(fmt.Sprintf("All entities retrieved successfully in %s", duration))
|
||||||
log.F("count", len(entities)),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return entities, nil
|
return entities, nil
|
||||||
}
|
}
|
||||||
@ -478,20 +443,18 @@ func (r *BaseRepositoryImpl[T]) Count(ctx context.Context) (int64, error) {
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "Count")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var count int64
|
var count int64
|
||||||
if err := r.db.WithContext(ctx).Model(new(T)).Count(&count).Error; err != nil {
|
if err := r.db.WithContext(ctx).Model(new(T)).Count(&count).Error; err != nil {
|
||||||
log.LogError("Failed to count entities",
|
log.Error(err, "Failed to count entities")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return 0, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return 0, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
log.LogDebug("Entity count retrieved successfully",
|
log.Debug(fmt.Sprintf("Entity count retrieved successfully in %s", duration))
|
||||||
log.F("count", count),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
@ -501,22 +464,20 @@ func (r *BaseRepositoryImpl[T]) CountWithOptions(ctx context.Context, options *d
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "CountWithOptions")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var count int64
|
var count int64
|
||||||
query := r.buildQuery(r.db.WithContext(ctx), options)
|
query := r.buildQuery(r.db.WithContext(ctx), options)
|
||||||
|
|
||||||
if err := query.Model(new(T)).Count(&count).Error; err != nil {
|
if err := query.Model(new(T)).Count(&count).Error; err != nil {
|
||||||
log.LogError("Failed to count entities with options",
|
log.Error(err, "Failed to count entities with options")
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return 0, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return 0, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
log.LogDebug("Entity count retrieved successfully with options",
|
log.Debug(fmt.Sprintf("Entity count retrieved successfully with options in %s", duration))
|
||||||
log.F("count", count),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
@ -526,6 +487,8 @@ func (r *BaseRepositoryImpl[T]) FindWithPreload(ctx context.Context, preloads []
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindWithPreload")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateID(id); err != nil {
|
if err := r.validateID(id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -540,25 +503,15 @@ func (r *BaseRepositoryImpl[T]) FindWithPreload(ctx context.Context, preloads []
|
|||||||
|
|
||||||
if err := query.First(&entity, id).Error; err != nil {
|
if err := query.First(&entity, id).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
log.LogDebug("Entity not found with preloads",
|
log.Debug(fmt.Sprintf("Entity with id %d not found with preloads in %s", id, time.Since(start)))
|
||||||
log.F("id", id),
|
|
||||||
log.F("preloads", preloads),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return nil, ErrEntityNotFound
|
return nil, ErrEntityNotFound
|
||||||
}
|
}
|
||||||
log.LogError("Failed to get entity with preloads",
|
log.Error(err, fmt.Sprintf("Failed to get entity with id %d with preloads", id))
|
||||||
log.F("id", id),
|
|
||||||
log.F("preloads", preloads),
|
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
log.LogDebug("Entity retrieved successfully with preloads",
|
log.Debug(fmt.Sprintf("Entity with id %d retrieved successfully with preloads in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("preloads", preloads),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return &entity, nil
|
return &entity, nil
|
||||||
}
|
}
|
||||||
@ -568,6 +521,8 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetAllForSync")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if batchSize <= 0 {
|
if batchSize <= 0 {
|
||||||
batchSize = config.Cfg.BatchSize
|
batchSize = config.Cfg.BatchSize
|
||||||
@ -583,20 +538,12 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
|
|||||||
start := time.Now()
|
start := time.Now()
|
||||||
var entities []T
|
var entities []T
|
||||||
if err := r.db.WithContext(ctx).Offset(offset).Limit(batchSize).Find(&entities).Error; err != nil {
|
if err := r.db.WithContext(ctx).Offset(offset).Limit(batchSize).Find(&entities).Error; err != nil {
|
||||||
log.LogError("Failed to get entities for sync",
|
log.Error(err, "Failed to get entities for sync")
|
||||||
log.F("batchSize", batchSize),
|
|
||||||
log.F("offset", offset),
|
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
log.LogDebug("Entities retrieved successfully for sync",
|
log.Debug(fmt.Sprintf("Entities retrieved successfully for sync in %s", duration))
|
||||||
log.F("batchSize", batchSize),
|
|
||||||
log.F("offset", offset),
|
|
||||||
log.F("count", len(entities)),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return entities, nil
|
return entities, nil
|
||||||
}
|
}
|
||||||
@ -606,6 +553,8 @@ func (r *BaseRepositoryImpl[T]) Exists(ctx context.Context, id uint) (bool, erro
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "Exists")
|
||||||
|
defer span.End()
|
||||||
if err := r.validateID(id); err != nil {
|
if err := r.validateID(id); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -613,20 +562,14 @@ func (r *BaseRepositoryImpl[T]) Exists(ctx context.Context, id uint) (bool, erro
|
|||||||
start := time.Now()
|
start := time.Now()
|
||||||
var count int64
|
var count int64
|
||||||
if err := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id).Count(&count).Error; err != nil {
|
if err := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id).Count(&count).Error; err != nil {
|
||||||
log.LogError("Failed to check entity existence",
|
log.Error(err, fmt.Sprintf("Failed to check entity existence for id %d", id))
|
||||||
log.F("id", id),
|
|
||||||
log.F("error", err),
|
|
||||||
log.F("duration", time.Since(start)))
|
|
||||||
return false, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
return false, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
exists := count > 0
|
exists := count > 0
|
||||||
|
|
||||||
log.LogDebug("Entity existence checked",
|
log.Debug(fmt.Sprintf("Entity existence checked for id %d in %s", id, duration))
|
||||||
log.F("id", id),
|
|
||||||
log.F("exists", exists),
|
|
||||||
log.F("duration", duration))
|
|
||||||
|
|
||||||
return exists, nil
|
return exists, nil
|
||||||
}
|
}
|
||||||
@ -636,15 +579,16 @@ func (r *BaseRepositoryImpl[T]) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "BeginTx")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
tx := r.db.WithContext(ctx).Begin()
|
tx := r.db.WithContext(ctx).Begin()
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
log.LogError("Failed to begin transaction",
|
log.Error(tx.Error, "Failed to begin transaction")
|
||||||
log.F("error", tx.Error))
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrTransactionFailed, tx.Error)
|
return nil, fmt.Errorf("%w: %v", ErrTransactionFailed, tx.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Transaction started successfully")
|
log.Debug("Transaction started successfully")
|
||||||
return tx, nil
|
return tx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,6 +597,8 @@ func (r *BaseRepositoryImpl[T]) WithTx(ctx context.Context, fn func(tx *gorm.DB)
|
|||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ctx, span := r.tracer.Start(ctx, "WithTx")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
tx, err := r.BeginTx(ctx)
|
tx, err := r.BeginTx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -662,29 +608,24 @@ func (r *BaseRepositoryImpl[T]) WithTx(ctx context.Context, fn func(tx *gorm.DB)
|
|||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
log.LogError("Transaction panic recovered",
|
log.Error(fmt.Errorf("panic recovered: %v", r), "Transaction panic recovered")
|
||||||
log.F("panic", r))
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := fn(tx); err != nil {
|
if err := fn(tx); err != nil {
|
||||||
if rbErr := tx.Rollback().Error; rbErr != nil {
|
if rbErr := tx.Rollback().Error; rbErr != nil {
|
||||||
log.LogError("Failed to rollback transaction",
|
log.Error(rbErr, fmt.Sprintf("Failed to rollback transaction after error: %v", err))
|
||||||
log.F("originalError", err),
|
|
||||||
log.F("rollbackError", rbErr))
|
|
||||||
return fmt.Errorf("transaction failed and rollback failed: %v (rollback: %v)", err, rbErr)
|
return fmt.Errorf("transaction failed and rollback failed: %v (rollback: %v)", err, rbErr)
|
||||||
}
|
}
|
||||||
log.LogDebug("Transaction rolled back due to error",
|
log.Debug(fmt.Sprintf("Transaction rolled back due to error: %v", err))
|
||||||
log.F("error", err))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
if err := tx.Commit().Error; err != nil {
|
||||||
log.LogError("Failed to commit transaction",
|
log.Error(err, "Failed to commit transaction")
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("%w: %v", ErrTransactionFailed, err)
|
return fmt.Errorf("%w: %v", ErrTransactionFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogDebug("Transaction committed successfully")
|
log.Debug("Transaction committed successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bookRepository struct {
|
type bookRepository struct {
|
||||||
domain.BaseRepository[domain.Book]
|
domain.BaseRepository[domain.Book]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBookRepository creates a new BookRepository.
|
// NewBookRepository creates a new BookRepository.
|
||||||
@ -18,11 +21,14 @@ func NewBookRepository(db *gorm.DB) domain.BookRepository {
|
|||||||
return &bookRepository{
|
return &bookRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Book](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Book](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("book.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByAuthorID finds books by author ID
|
// ListByAuthorID finds books by author ID
|
||||||
func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]domain.Book, error) {
|
func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]domain.Book, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByAuthorID")
|
||||||
|
defer span.End()
|
||||||
var books []domain.Book
|
var books []domain.Book
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.book_id = books.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.book_id = books.id").
|
||||||
Where("book_authors.author_id = ?", authorID).
|
Where("book_authors.author_id = ?", authorID).
|
||||||
@ -34,6 +40,8 @@ func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]d
|
|||||||
|
|
||||||
// ListByPublisherID finds books by publisher ID
|
// ListByPublisherID finds books by publisher ID
|
||||||
func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint) ([]domain.Book, error) {
|
func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint) ([]domain.Book, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByPublisherID")
|
||||||
|
defer span.End()
|
||||||
var books []domain.Book
|
var books []domain.Book
|
||||||
if err := r.db.WithContext(ctx).Where("publisher_id = ?", publisherID).Find(&books).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("publisher_id = ?", publisherID).Find(&books).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -43,6 +51,8 @@ func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint
|
|||||||
|
|
||||||
// ListByWorkID finds books by work ID
|
// ListByWorkID finds books by work ID
|
||||||
func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Book, error) {
|
func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Book, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var books []domain.Book
|
var books []domain.Book
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN book_works ON book_works.book_id = books.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN book_works ON book_works.book_id = books.id").
|
||||||
Where("book_works.work_id = ?", workID).
|
Where("book_works.work_id = ?", workID).
|
||||||
@ -54,6 +64,8 @@ func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]domai
|
|||||||
|
|
||||||
// FindByISBN finds a book by ISBN
|
// FindByISBN finds a book by ISBN
|
||||||
func (r *bookRepository) FindByISBN(ctx context.Context, isbn string) (*domain.Book, error) {
|
func (r *bookRepository) FindByISBN(ctx context.Context, isbn string) (*domain.Book, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByISBN")
|
||||||
|
defer span.End()
|
||||||
var book domain.Book
|
var book domain.Book
|
||||||
if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&book).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&book).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bookmarkRepository struct {
|
type bookmarkRepository struct {
|
||||||
domain.BaseRepository[domain.Bookmark]
|
domain.BaseRepository[domain.Bookmark]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBookmarkRepository creates a new BookmarkRepository.
|
// NewBookmarkRepository creates a new BookmarkRepository.
|
||||||
@ -17,11 +20,14 @@ func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository {
|
|||||||
return &bookmarkRepository{
|
return &bookmarkRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("bookmark.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByUserID finds bookmarks by user ID
|
// ListByUserID finds bookmarks by user ID
|
||||||
func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) {
|
func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
||||||
|
defer span.End()
|
||||||
var bookmarks []domain.Bookmark
|
var bookmarks []domain.Bookmark
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&bookmarks).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&bookmarks).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -31,6 +37,8 @@ func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]d
|
|||||||
|
|
||||||
// ListByWorkID finds bookmarks by work ID
|
// ListByWorkID finds bookmarks by work ID
|
||||||
func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) {
|
func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var bookmarks []domain.Bookmark
|
var bookmarks []domain.Bookmark
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&bookmarks).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&bookmarks).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type categoryRepository struct {
|
type categoryRepository struct {
|
||||||
domain.BaseRepository[domain.Category]
|
domain.BaseRepository[domain.Category]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCategoryRepository creates a new CategoryRepository.
|
// NewCategoryRepository creates a new CategoryRepository.
|
||||||
@ -18,11 +21,14 @@ func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository {
|
|||||||
return &categoryRepository{
|
return &categoryRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Category](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Category](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("category.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByName finds a category by name
|
// FindByName finds a category by name
|
||||||
func (r *categoryRepository) FindByName(ctx context.Context, name string) (*domain.Category, error) {
|
func (r *categoryRepository) FindByName(ctx context.Context, name string) (*domain.Category, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByName")
|
||||||
|
defer span.End()
|
||||||
var category domain.Category
|
var category domain.Category
|
||||||
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&category).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&category).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@ -35,6 +41,8 @@ func (r *categoryRepository) FindByName(ctx context.Context, name string) (*doma
|
|||||||
|
|
||||||
// ListByWorkID finds categories by work ID
|
// ListByWorkID finds categories by work ID
|
||||||
func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) {
|
func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var categories []domain.Category
|
var categories []domain.Category
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN work_categories ON work_categories.category_id = categories.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN work_categories ON work_categories.category_id = categories.id").
|
||||||
Where("work_categories.work_id = ?", workID).
|
Where("work_categories.work_id = ?", workID).
|
||||||
@ -46,6 +54,8 @@ func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]d
|
|||||||
|
|
||||||
// ListByParentID finds categories by parent ID
|
// ListByParentID finds categories by parent ID
|
||||||
func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) {
|
func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByParentID")
|
||||||
|
defer span.End()
|
||||||
var categories []domain.Category
|
var categories []domain.Category
|
||||||
if parentID == nil {
|
if parentID == nil {
|
||||||
if err := r.db.WithContext(ctx).Where("parent_id IS NULL").Find(&categories).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("parent_id IS NULL").Find(&categories).Error; err != nil {
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type collectionRepository struct {
|
type collectionRepository struct {
|
||||||
domain.BaseRepository[domain.Collection]
|
domain.BaseRepository[domain.Collection]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCollectionRepository creates a new CollectionRepository.
|
// NewCollectionRepository creates a new CollectionRepository.
|
||||||
@ -17,11 +20,14 @@ func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository {
|
|||||||
return &collectionRepository{
|
return &collectionRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("collection.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByUserID finds collections by user ID
|
// ListByUserID finds collections by user ID
|
||||||
func (r *collectionRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Collection, error) {
|
func (r *collectionRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Collection, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
||||||
|
defer span.End()
|
||||||
var collections []domain.Collection
|
var collections []domain.Collection
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&collections).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&collections).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -31,16 +37,22 @@ func (r *collectionRepository) ListByUserID(ctx context.Context, userID uint) ([
|
|||||||
|
|
||||||
// AddWorkToCollection adds a work to a collection
|
// AddWorkToCollection adds a work to a collection
|
||||||
func (r *collectionRepository) AddWorkToCollection(ctx context.Context, collectionID uint, workID uint) error {
|
func (r *collectionRepository) AddWorkToCollection(ctx context.Context, collectionID uint, workID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddWorkToCollection")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("INSERT INTO collection_works (collection_id, work_id) VALUES (?, ?) ON CONFLICT DO NOTHING", collectionID, workID).Error
|
return r.db.WithContext(ctx).Exec("INSERT INTO collection_works (collection_id, work_id) VALUES (?, ?) ON CONFLICT DO NOTHING", collectionID, workID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveWorkFromCollection removes a work from a collection
|
// RemoveWorkFromCollection removes a work from a collection
|
||||||
func (r *collectionRepository) RemoveWorkFromCollection(ctx context.Context, collectionID uint, workID uint) error {
|
func (r *collectionRepository) RemoveWorkFromCollection(ctx context.Context, collectionID uint, workID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveWorkFromCollection")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("DELETE FROM collection_works WHERE collection_id = ? AND work_id = ?", collectionID, workID).Error
|
return r.db.WithContext(ctx).Exec("DELETE FROM collection_works WHERE collection_id = ? AND work_id = ?", collectionID, workID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPublic finds public collections
|
// ListPublic finds public collections
|
||||||
func (r *collectionRepository) ListPublic(ctx context.Context) ([]domain.Collection, error) {
|
func (r *collectionRepository) ListPublic(ctx context.Context) ([]domain.Collection, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListPublic")
|
||||||
|
defer span.End()
|
||||||
var collections []domain.Collection
|
var collections []domain.Collection
|
||||||
if err := r.db.WithContext(ctx).Where("is_public = ?", true).Find(&collections).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("is_public = ?", true).Find(&collections).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -50,6 +62,8 @@ func (r *collectionRepository) ListPublic(ctx context.Context) ([]domain.Collect
|
|||||||
|
|
||||||
// ListByWorkID finds collections by work ID
|
// ListByWorkID finds collections by work ID
|
||||||
func (r *collectionRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) {
|
func (r *collectionRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var collections []domain.Collection
|
var collections []domain.Collection
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN collection_works ON collection_works.collection_id = collections.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN collection_works ON collection_works.collection_id = collections.id").
|
||||||
Where("collection_works.work_id = ?", workID).
|
Where("collection_works.work_id = ?", workID).
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type commentRepository struct {
|
type commentRepository struct {
|
||||||
domain.BaseRepository[domain.Comment]
|
domain.BaseRepository[domain.Comment]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommentRepository creates a new CommentRepository.
|
// NewCommentRepository creates a new CommentRepository.
|
||||||
@ -17,11 +20,14 @@ func NewCommentRepository(db *gorm.DB) domain.CommentRepository {
|
|||||||
return &commentRepository{
|
return &commentRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("comment.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByUserID finds comments by user ID
|
// ListByUserID finds comments by user ID
|
||||||
func (r *commentRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Comment, error) {
|
func (r *commentRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Comment, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
||||||
|
defer span.End()
|
||||||
var comments []domain.Comment
|
var comments []domain.Comment
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&comments).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&comments).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -31,6 +37,8 @@ func (r *commentRepository) ListByUserID(ctx context.Context, userID uint) ([]do
|
|||||||
|
|
||||||
// ListByWorkID finds comments by work ID
|
// ListByWorkID finds comments by work ID
|
||||||
func (r *commentRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Comment, error) {
|
func (r *commentRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Comment, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var comments []domain.Comment
|
var comments []domain.Comment
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&comments).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&comments).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -40,6 +48,8 @@ func (r *commentRepository) ListByWorkID(ctx context.Context, workID uint) ([]do
|
|||||||
|
|
||||||
// ListByTranslationID finds comments by translation ID
|
// ListByTranslationID finds comments by translation ID
|
||||||
func (r *commentRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Comment, error) {
|
func (r *commentRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Comment, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByTranslationID")
|
||||||
|
defer span.End()
|
||||||
var comments []domain.Comment
|
var comments []domain.Comment
|
||||||
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&comments).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&comments).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -49,6 +59,8 @@ func (r *commentRepository) ListByTranslationID(ctx context.Context, translation
|
|||||||
|
|
||||||
// ListByParentID finds comments by parent ID
|
// ListByParentID finds comments by parent ID
|
||||||
func (r *commentRepository) ListByParentID(ctx context.Context, parentID uint) ([]domain.Comment, error) {
|
func (r *commentRepository) ListByParentID(ctx context.Context, parentID uint) ([]domain.Comment, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByParentID")
|
||||||
|
defer span.End()
|
||||||
var comments []domain.Comment
|
var comments []domain.Comment
|
||||||
if err := r.db.WithContext(ctx).Where("parent_id = ?", parentID).Find(&comments).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("parent_id = ?", parentID).Find(&comments).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contributionRepository struct {
|
type contributionRepository struct {
|
||||||
domain.BaseRepository[domain.Contribution]
|
domain.BaseRepository[domain.Contribution]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContributionRepository creates a new ContributionRepository.
|
// NewContributionRepository creates a new ContributionRepository.
|
||||||
@ -17,11 +20,14 @@ func NewContributionRepository(db *gorm.DB) domain.ContributionRepository {
|
|||||||
return &contributionRepository{
|
return &contributionRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("contribution.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByUserID finds contributions by user ID
|
// ListByUserID finds contributions by user ID
|
||||||
func (r *contributionRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Contribution, error) {
|
func (r *contributionRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Contribution, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
||||||
|
defer span.End()
|
||||||
var contributions []domain.Contribution
|
var contributions []domain.Contribution
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&contributions).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&contributions).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -31,6 +37,8 @@ func (r *contributionRepository) ListByUserID(ctx context.Context, userID uint)
|
|||||||
|
|
||||||
// ListByReviewerID finds contributions by reviewer ID
|
// ListByReviewerID finds contributions by reviewer ID
|
||||||
func (r *contributionRepository) ListByReviewerID(ctx context.Context, reviewerID uint) ([]domain.Contribution, error) {
|
func (r *contributionRepository) ListByReviewerID(ctx context.Context, reviewerID uint) ([]domain.Contribution, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByReviewerID")
|
||||||
|
defer span.End()
|
||||||
var contributions []domain.Contribution
|
var contributions []domain.Contribution
|
||||||
if err := r.db.WithContext(ctx).Where("reviewer_id = ?", reviewerID).Find(&contributions).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("reviewer_id = ?", reviewerID).Find(&contributions).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -40,6 +48,8 @@ func (r *contributionRepository) ListByReviewerID(ctx context.Context, reviewerI
|
|||||||
|
|
||||||
// ListByWorkID finds contributions by work ID
|
// ListByWorkID finds contributions by work ID
|
||||||
func (r *contributionRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Contribution, error) {
|
func (r *contributionRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Contribution, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var contributions []domain.Contribution
|
var contributions []domain.Contribution
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&contributions).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&contributions).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -49,6 +59,8 @@ func (r *contributionRepository) ListByWorkID(ctx context.Context, workID uint)
|
|||||||
|
|
||||||
// ListByTranslationID finds contributions by translation ID
|
// ListByTranslationID finds contributions by translation ID
|
||||||
func (r *contributionRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Contribution, error) {
|
func (r *contributionRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Contribution, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByTranslationID")
|
||||||
|
defer span.End()
|
||||||
var contributions []domain.Contribution
|
var contributions []domain.Contribution
|
||||||
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&contributions).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&contributions).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -58,6 +70,8 @@ func (r *contributionRepository) ListByTranslationID(ctx context.Context, transl
|
|||||||
|
|
||||||
// ListByStatus finds contributions by status
|
// ListByStatus finds contributions by status
|
||||||
func (r *contributionRepository) ListByStatus(ctx context.Context, status string) ([]domain.Contribution, error) {
|
func (r *contributionRepository) ListByStatus(ctx context.Context, status string) ([]domain.Contribution, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByStatus")
|
||||||
|
defer span.End()
|
||||||
var contributions []domain.Contribution
|
var contributions []domain.Contribution
|
||||||
if err := r.db.WithContext(ctx).Where("status = ?", status).Find(&contributions).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("status = ?", status).Find(&contributions).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type copyrightClaimRepository struct {
|
type copyrightClaimRepository struct {
|
||||||
domain.BaseRepository[domain.CopyrightClaim]
|
domain.BaseRepository[domain.CopyrightClaim]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCopyrightClaimRepository creates a new CopyrightClaimRepository.
|
// NewCopyrightClaimRepository creates a new CopyrightClaimRepository.
|
||||||
@ -17,11 +20,14 @@ func NewCopyrightClaimRepository(db *gorm.DB) domain.CopyrightClaimRepository {
|
|||||||
return ©rightClaimRepository{
|
return ©rightClaimRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("copyright_claim.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByWorkID finds claims by work ID
|
// ListByWorkID finds claims by work ID
|
||||||
func (r *copyrightClaimRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.CopyrightClaim, error) {
|
func (r *copyrightClaimRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.CopyrightClaim, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var claims []domain.CopyrightClaim
|
var claims []domain.CopyrightClaim
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&claims).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&claims).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -31,6 +37,8 @@ func (r *copyrightClaimRepository) ListByWorkID(ctx context.Context, workID uint
|
|||||||
|
|
||||||
// ListByUserID finds claims by user ID
|
// ListByUserID finds claims by user ID
|
||||||
func (r *copyrightClaimRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.CopyrightClaim, error) {
|
func (r *copyrightClaimRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.CopyrightClaim, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
||||||
|
defer span.End()
|
||||||
var claims []domain.CopyrightClaim
|
var claims []domain.CopyrightClaim
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&claims).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&claims).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type copyrightRepository struct {
|
type copyrightRepository struct {
|
||||||
domain.BaseRepository[domain.Copyright]
|
domain.BaseRepository[domain.Copyright]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCopyrightRepository creates a new CopyrightRepository.
|
// NewCopyrightRepository creates a new CopyrightRepository.
|
||||||
@ -18,16 +21,21 @@ func NewCopyrightRepository(db *gorm.DB) domain.CopyrightRepository {
|
|||||||
return ©rightRepository{
|
return ©rightRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("copyright.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTranslation adds a translation to a copyright
|
// AddTranslation adds a translation to a copyright
|
||||||
func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error {
|
func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddTranslation")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Create(translation).Error
|
return r.db.WithContext(ctx).Create(translation).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTranslations gets all translations for a copyright
|
// GetTranslations gets all translations for a copyright
|
||||||
func (r *copyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
func (r *copyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetTranslations")
|
||||||
|
defer span.End()
|
||||||
var translations []domain.CopyrightTranslation
|
var translations []domain.CopyrightTranslation
|
||||||
err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(&translations).Error
|
err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(&translations).Error
|
||||||
return translations, err
|
return translations, err
|
||||||
@ -35,6 +43,8 @@ func (r *copyrightRepository) GetTranslations(ctx context.Context, copyrightID u
|
|||||||
|
|
||||||
// GetTranslationByLanguage gets a specific translation by language code
|
// GetTranslationByLanguage gets a specific translation by language code
|
||||||
func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetTranslationByLanguage")
|
||||||
|
defer span.End()
|
||||||
var translation domain.CopyrightTranslation
|
var translation domain.CopyrightTranslation
|
||||||
err := r.db.WithContext(ctx).Where("copyright_id = ? AND language_code = ?", copyrightID, languageCode).First(&translation).Error
|
err := r.db.WithContext(ctx).Where("copyright_id = ? AND language_code = ?", copyrightID, languageCode).First(&translation).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -47,41 +57,61 @@ func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copy
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
func (r *copyrightRepository) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddCopyrightToWork")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("INSERT INTO work_copyrights (work_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", workID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("INSERT INTO work_copyrights (work_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", workID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
func (r *copyrightRepository) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveCopyrightFromWork")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("DELETE FROM work_copyrights WHERE work_id = ? AND copyright_id = ?", workID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("DELETE FROM work_copyrights WHERE work_id = ? AND copyright_id = ?", workID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
func (r *copyrightRepository) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddCopyrightToAuthor")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("INSERT INTO author_copyrights (author_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", authorID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("INSERT INTO author_copyrights (author_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", authorID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
func (r *copyrightRepository) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveCopyrightFromAuthor")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("DELETE FROM author_copyrights WHERE author_id = ? AND copyright_id = ?", authorID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("DELETE FROM author_copyrights WHERE author_id = ? AND copyright_id = ?", authorID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
func (r *copyrightRepository) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddCopyrightToBook")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("INSERT INTO book_copyrights (book_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", bookID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("INSERT INTO book_copyrights (book_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", bookID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
func (r *copyrightRepository) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveCopyrightFromBook")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("DELETE FROM book_copyrights WHERE book_id = ? AND copyright_id = ?", bookID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("DELETE FROM book_copyrights WHERE book_id = ? AND copyright_id = ?", bookID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
func (r *copyrightRepository) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddCopyrightToPublisher")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("INSERT INTO publisher_copyrights (publisher_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", publisherID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("INSERT INTO publisher_copyrights (publisher_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", publisherID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
func (r *copyrightRepository) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveCopyrightFromPublisher")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("DELETE FROM publisher_copyrights WHERE publisher_id = ? AND copyright_id = ?", publisherID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("DELETE FROM publisher_copyrights WHERE publisher_id = ? AND copyright_id = ?", publisherID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
func (r *copyrightRepository) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddCopyrightToSource")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("INSERT INTO source_copyrights (source_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", sourceID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("INSERT INTO source_copyrights (source_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", sourceID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *copyrightRepository) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
func (r *copyrightRepository) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveCopyrightFromSource")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Exec("DELETE FROM source_copyrights WHERE source_id = ? AND copyright_id = ?", sourceID, copyrightID).Error
|
return r.db.WithContext(ctx).Exec("DELETE FROM source_copyrights WHERE source_id = ? AND copyright_id = ?", sourceID, copyrightID).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type edgeRepository struct {
|
type edgeRepository struct {
|
||||||
domain.BaseRepository[domain.Edge]
|
domain.BaseRepository[domain.Edge]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEdgeRepository creates a new EdgeRepository.
|
// NewEdgeRepository creates a new EdgeRepository.
|
||||||
@ -17,11 +20,14 @@ func NewEdgeRepository(db *gorm.DB) domain.EdgeRepository {
|
|||||||
return &edgeRepository{
|
return &edgeRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Edge](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Edge](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("edge.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListBySource finds edges by source table and ID
|
// ListBySource finds edges by source table and ID
|
||||||
func (r *edgeRepository) ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]domain.Edge, error) {
|
func (r *edgeRepository) ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]domain.Edge, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListBySource")
|
||||||
|
defer span.End()
|
||||||
var edges []domain.Edge
|
var edges []domain.Edge
|
||||||
if err := r.db.WithContext(ctx).Where("source_table = ? AND source_id = ?", sourceTable, sourceID).Find(&edges).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("source_table = ? AND source_id = ?", sourceTable, sourceID).Find(&edges).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type editionRepository struct {
|
type editionRepository struct {
|
||||||
domain.BaseRepository[domain.Edition]
|
domain.BaseRepository[domain.Edition]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEditionRepository creates a new EditionRepository.
|
// NewEditionRepository creates a new EditionRepository.
|
||||||
@ -18,11 +21,14 @@ func NewEditionRepository(db *gorm.DB) domain.EditionRepository {
|
|||||||
return &editionRepository{
|
return &editionRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("edition.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByBookID finds editions by book ID
|
// ListByBookID finds editions by book ID
|
||||||
func (r *editionRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Edition, error) {
|
func (r *editionRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Edition, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByBookID")
|
||||||
|
defer span.End()
|
||||||
var editions []domain.Edition
|
var editions []domain.Edition
|
||||||
if err := r.db.WithContext(ctx).Where("book_id = ?", bookID).Find(&editions).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("book_id = ?", bookID).Find(&editions).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -32,6 +38,8 @@ func (r *editionRepository) ListByBookID(ctx context.Context, bookID uint) ([]do
|
|||||||
|
|
||||||
// FindByISBN finds an edition by ISBN
|
// FindByISBN finds an edition by ISBN
|
||||||
func (r *editionRepository) FindByISBN(ctx context.Context, isbn string) (*domain.Edition, error) {
|
func (r *editionRepository) FindByISBN(ctx context.Context, isbn string) (*domain.Edition, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByISBN")
|
||||||
|
defer span.End()
|
||||||
var edition domain.Edition
|
var edition domain.Edition
|
||||||
if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&edition).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&edition).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
@ -6,12 +6,15 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type emailVerificationRepository struct {
|
type emailVerificationRepository struct {
|
||||||
domain.BaseRepository[domain.EmailVerification]
|
domain.BaseRepository[domain.EmailVerification]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEmailVerificationRepository creates a new EmailVerificationRepository.
|
// NewEmailVerificationRepository creates a new EmailVerificationRepository.
|
||||||
@ -19,11 +22,14 @@ func NewEmailVerificationRepository(db *gorm.DB) domain.EmailVerificationReposit
|
|||||||
return &emailVerificationRepository{
|
return &emailVerificationRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("email_verification.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByToken finds a verification by token (only unused and non-expired)
|
// GetByToken finds a verification by token (only unused and non-expired)
|
||||||
func (r *emailVerificationRepository) GetByToken(ctx context.Context, token string) (*domain.EmailVerification, error) {
|
func (r *emailVerificationRepository) GetByToken(ctx context.Context, token string) (*domain.EmailVerification, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByToken")
|
||||||
|
defer span.End()
|
||||||
var verification domain.EmailVerification
|
var verification domain.EmailVerification
|
||||||
if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&verification).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&verification).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@ -36,6 +42,8 @@ func (r *emailVerificationRepository) GetByToken(ctx context.Context, token stri
|
|||||||
|
|
||||||
// GetByUserID finds verifications by user ID
|
// GetByUserID finds verifications by user ID
|
||||||
func (r *emailVerificationRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.EmailVerification, error) {
|
func (r *emailVerificationRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.EmailVerification, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByUserID")
|
||||||
|
defer span.End()
|
||||||
var verifications []domain.EmailVerification
|
var verifications []domain.EmailVerification
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&verifications).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&verifications).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -45,6 +53,8 @@ func (r *emailVerificationRepository) GetByUserID(ctx context.Context, userID ui
|
|||||||
|
|
||||||
// DeleteExpired deletes expired verifications
|
// DeleteExpired deletes expired verifications
|
||||||
func (r *emailVerificationRepository) DeleteExpired(ctx context.Context) error {
|
func (r *emailVerificationRepository) DeleteExpired(ctx context.Context) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "DeleteExpired")
|
||||||
|
defer span.End()
|
||||||
if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.EmailVerification{}).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.EmailVerification{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -53,6 +63,8 @@ func (r *emailVerificationRepository) DeleteExpired(ctx context.Context) error {
|
|||||||
|
|
||||||
// MarkAsUsed marks a verification as used
|
// MarkAsUsed marks a verification as used
|
||||||
func (r *emailVerificationRepository) MarkAsUsed(ctx context.Context, id uint) error {
|
func (r *emailVerificationRepository) MarkAsUsed(ctx context.Context, id uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "MarkAsUsed")
|
||||||
|
defer span.End()
|
||||||
if err := r.db.WithContext(ctx).Model(&domain.EmailVerification{}).Where("id = ?", id).Update("used", true).Error; err != nil {
|
if err := r.db.WithContext(ctx).Model(&domain.EmailVerification{}).Where("id = ?", id).Update("used", true).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type likeRepository struct {
|
type likeRepository struct {
|
||||||
domain.BaseRepository[domain.Like]
|
domain.BaseRepository[domain.Like]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLikeRepository creates a new LikeRepository.
|
// NewLikeRepository creates a new LikeRepository.
|
||||||
@ -17,11 +20,14 @@ func NewLikeRepository(db *gorm.DB) domain.LikeRepository {
|
|||||||
return &likeRepository{
|
return &likeRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Like](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Like](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("like.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByUserID finds likes by user ID
|
// ListByUserID finds likes by user ID
|
||||||
func (r *likeRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Like, error) {
|
func (r *likeRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Like, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
||||||
|
defer span.End()
|
||||||
var likes []domain.Like
|
var likes []domain.Like
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&likes).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&likes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -31,6 +37,8 @@ func (r *likeRepository) ListByUserID(ctx context.Context, userID uint) ([]domai
|
|||||||
|
|
||||||
// ListByWorkID finds likes by work ID
|
// ListByWorkID finds likes by work ID
|
||||||
func (r *likeRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Like, error) {
|
func (r *likeRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Like, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var likes []domain.Like
|
var likes []domain.Like
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&likes).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).Find(&likes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -40,6 +48,8 @@ func (r *likeRepository) ListByWorkID(ctx context.Context, workID uint) ([]domai
|
|||||||
|
|
||||||
// ListByTranslationID finds likes by translation ID
|
// ListByTranslationID finds likes by translation ID
|
||||||
func (r *likeRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Like, error) {
|
func (r *likeRepository) ListByTranslationID(ctx context.Context, translationID uint) ([]domain.Like, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByTranslationID")
|
||||||
|
defer span.End()
|
||||||
var likes []domain.Like
|
var likes []domain.Like
|
||||||
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&likes).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("translation_id = ?", translationID).Find(&likes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -49,6 +59,8 @@ func (r *likeRepository) ListByTranslationID(ctx context.Context, translationID
|
|||||||
|
|
||||||
// ListByCommentID finds likes by comment ID
|
// ListByCommentID finds likes by comment ID
|
||||||
func (r *likeRepository) ListByCommentID(ctx context.Context, commentID uint) ([]domain.Like, error) {
|
func (r *likeRepository) ListByCommentID(ctx context.Context, commentID uint) ([]domain.Like, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByCommentID")
|
||||||
|
defer span.End()
|
||||||
var likes []domain.Like
|
var likes []domain.Like
|
||||||
if err := r.db.WithContext(ctx).Where("comment_id = ?", commentID).Find(&likes).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("comment_id = ?", commentID).Find(&likes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -5,18 +5,26 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/localization"
|
"tercul/internal/domain/localization"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type localizationRepository struct {
|
type localizationRepository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocalizationRepository(db *gorm.DB) localization.LocalizationRepository {
|
func NewLocalizationRepository(db *gorm.DB) localization.LocalizationRepository {
|
||||||
return &localizationRepository{db: db}
|
return &localizationRepository{
|
||||||
|
db: db,
|
||||||
|
tracer: otel.Tracer("localization.repository"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *localizationRepository) GetTranslation(ctx context.Context, key string, language string) (string, error) {
|
func (r *localizationRepository) GetTranslation(ctx context.Context, key string, language string) (string, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetTranslation")
|
||||||
|
defer span.End()
|
||||||
var l localization.Localization
|
var l localization.Localization
|
||||||
err := r.db.WithContext(ctx).Where("key = ? AND language = ?", key, language).First(&l).Error
|
err := r.db.WithContext(ctx).Where("key = ? AND language = ?", key, language).First(&l).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -26,6 +34,8 @@ func (r *localizationRepository) GetTranslation(ctx context.Context, key string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *localizationRepository) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) {
|
func (r *localizationRepository) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetTranslations")
|
||||||
|
defer span.End()
|
||||||
var localizations []localization.Localization
|
var localizations []localization.Localization
|
||||||
err := r.db.WithContext(ctx).Where("key IN ? AND language = ?", keys, language).Find(&localizations).Error
|
err := r.db.WithContext(ctx).Where("key IN ? AND language = ?", keys, language).Find(&localizations).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -39,6 +49,8 @@ func (r *localizationRepository) GetTranslations(ctx context.Context, keys []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *localizationRepository) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) {
|
func (r *localizationRepository) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetAuthorBiography")
|
||||||
|
defer span.End()
|
||||||
var translation domain.Translation
|
var translation domain.Translation
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Where("translatable_type = ? AND translatable_id = ? AND language = ?", "authors", authorID, language).
|
Where("translatable_type = ? AND translatable_id = ? AND language = ?", "authors", authorID, language).
|
||||||
@ -51,3 +63,17 @@ func (r *localizationRepository) GetAuthorBiography(ctx context.Context, authorI
|
|||||||
}
|
}
|
||||||
return translation.Content, nil
|
return translation.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *localizationRepository) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
||||||
|
var translation domain.Translation
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("translatable_type = ? AND translatable_id = ? AND language = ?", "works", workID, language).
|
||||||
|
First(&translation).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return translation.Content, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/work"
|
"tercul/internal/domain/work"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type monetizationRepository struct {
|
type monetizationRepository struct {
|
||||||
domain.BaseRepository[domain.Monetization]
|
domain.BaseRepository[domain.Monetization]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMonetizationRepository creates a new MonetizationRepository.
|
// NewMonetizationRepository creates a new MonetizationRepository.
|
||||||
@ -18,64 +21,85 @@ func NewMonetizationRepository(db *gorm.DB) domain.MonetizationRepository {
|
|||||||
return &monetizationRepository{
|
return &monetizationRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("monetization.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
|
func (r *monetizationRepository) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddMonetizationToWork")
|
||||||
|
defer span.End()
|
||||||
workRecord := &work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
|
workRecord := &work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(workRecord).Association("Monetizations").Append(monetization)
|
return r.db.WithContext(ctx).Model(workRecord).Association("Monetizations").Append(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
|
func (r *monetizationRepository) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveMonetizationFromWork")
|
||||||
|
defer span.End()
|
||||||
workRecord := &work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
|
workRecord := &work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(workRecord).Association("Monetizations").Delete(monetization)
|
return r.db.WithContext(ctx).Model(workRecord).Association("Monetizations").Delete(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
func (r *monetizationRepository) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddMonetizationToAuthor")
|
||||||
|
defer span.End()
|
||||||
author := &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: authorID}}}
|
author := &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: authorID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(author).Association("Monetizations").Append(monetization)
|
return r.db.WithContext(ctx).Model(author).Association("Monetizations").Append(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
func (r *monetizationRepository) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveMonetizationFromAuthor")
|
||||||
|
defer span.End()
|
||||||
author := &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: authorID}}}
|
author := &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: authorID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(author).Association("Monetizations").Delete(monetization)
|
return r.db.WithContext(ctx).Model(author).Association("Monetizations").Delete(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
func (r *monetizationRepository) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddMonetizationToBook")
|
||||||
|
defer span.End()
|
||||||
book := &domain.Book{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: bookID}}}
|
book := &domain.Book{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: bookID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(book).Association("Monetizations").Append(monetization)
|
return r.db.WithContext(ctx).Model(book).Association("Monetizations").Append(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
func (r *monetizationRepository) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveMonetizationFromBook")
|
||||||
|
defer span.End()
|
||||||
book := &domain.Book{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: bookID}}}
|
book := &domain.Book{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: bookID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(book).Association("Monetizations").Delete(monetization)
|
return r.db.WithContext(ctx).Model(book).Association("Monetizations").Delete(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
func (r *monetizationRepository) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddMonetizationToPublisher")
|
||||||
|
defer span.End()
|
||||||
publisher := &domain.Publisher{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: publisherID}}}
|
publisher := &domain.Publisher{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: publisherID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(publisher).Association("Monetizations").Append(monetization)
|
return r.db.WithContext(ctx).Model(publisher).Association("Monetizations").Append(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
func (r *monetizationRepository) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveMonetizationFromPublisher")
|
||||||
|
defer span.End()
|
||||||
publisher := &domain.Publisher{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: publisherID}}}
|
publisher := &domain.Publisher{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: publisherID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(publisher).Association("Monetizations").Delete(monetization)
|
return r.db.WithContext(ctx).Model(publisher).Association("Monetizations").Delete(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
func (r *monetizationRepository) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "AddMonetizationToSource")
|
||||||
|
defer span.End()
|
||||||
source := &domain.Source{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: sourceID}}}
|
source := &domain.Source{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: sourceID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(source).Association("Monetizations").Append(monetization)
|
return r.db.WithContext(ctx).Model(source).Association("Monetizations").Append(monetization)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *monetizationRepository) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
func (r *monetizationRepository) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "RemoveMonetizationFromSource")
|
||||||
|
defer span.End()
|
||||||
source := &domain.Source{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: sourceID}}}
|
source := &domain.Source{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: sourceID}}}
|
||||||
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
monetization := &domain.Monetization{BaseModel: domain.BaseModel{ID: monetizationID}}
|
||||||
return r.db.WithContext(ctx).Model(source).Association("Monetizations").Delete(monetization)
|
return r.db.WithContext(ctx).Model(source).Association("Monetizations").Delete(monetization)
|
||||||
|
|||||||
@ -6,12 +6,15 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type passwordResetRepository struct {
|
type passwordResetRepository struct {
|
||||||
domain.BaseRepository[domain.PasswordReset]
|
domain.BaseRepository[domain.PasswordReset]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPasswordResetRepository creates a new PasswordResetRepository.
|
// NewPasswordResetRepository creates a new PasswordResetRepository.
|
||||||
@ -19,11 +22,14 @@ func NewPasswordResetRepository(db *gorm.DB) domain.PasswordResetRepository {
|
|||||||
return &passwordResetRepository{
|
return &passwordResetRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("password_reset.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByToken finds a reset by token (only unused and non-expired)
|
// GetByToken finds a reset by token (only unused and non-expired)
|
||||||
func (r *passwordResetRepository) GetByToken(ctx context.Context, token string) (*domain.PasswordReset, error) {
|
func (r *passwordResetRepository) GetByToken(ctx context.Context, token string) (*domain.PasswordReset, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByToken")
|
||||||
|
defer span.End()
|
||||||
var reset domain.PasswordReset
|
var reset domain.PasswordReset
|
||||||
if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&reset).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&reset).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@ -36,6 +42,8 @@ func (r *passwordResetRepository) GetByToken(ctx context.Context, token string)
|
|||||||
|
|
||||||
// GetByUserID finds resets by user ID
|
// GetByUserID finds resets by user ID
|
||||||
func (r *passwordResetRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.PasswordReset, error) {
|
func (r *passwordResetRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.PasswordReset, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByUserID")
|
||||||
|
defer span.End()
|
||||||
var resets []domain.PasswordReset
|
var resets []domain.PasswordReset
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&resets).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&resets).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -45,6 +53,8 @@ func (r *passwordResetRepository) GetByUserID(ctx context.Context, userID uint)
|
|||||||
|
|
||||||
// DeleteExpired deletes expired resets
|
// DeleteExpired deletes expired resets
|
||||||
func (r *passwordResetRepository) DeleteExpired(ctx context.Context) error {
|
func (r *passwordResetRepository) DeleteExpired(ctx context.Context) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "DeleteExpired")
|
||||||
|
defer span.End()
|
||||||
if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.PasswordReset{}).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.PasswordReset{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -53,6 +63,8 @@ func (r *passwordResetRepository) DeleteExpired(ctx context.Context) error {
|
|||||||
|
|
||||||
// MarkAsUsed marks a reset as used
|
// MarkAsUsed marks a reset as used
|
||||||
func (r *passwordResetRepository) MarkAsUsed(ctx context.Context, id uint) error {
|
func (r *passwordResetRepository) MarkAsUsed(ctx context.Context, id uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "MarkAsUsed")
|
||||||
|
defer span.End()
|
||||||
if err := r.db.WithContext(ctx).Model(&domain.PasswordReset{}).Where("id = ?", id).Update("used", true).Error; err != nil {
|
if err := r.db.WithContext(ctx).Model(&domain.PasswordReset{}).Where("id = ?", id).Update("used", true).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type placeRepository struct {
|
type placeRepository struct {
|
||||||
domain.BaseRepository[domain.Place]
|
domain.BaseRepository[domain.Place]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPlaceRepository creates a new PlaceRepository.
|
// NewPlaceRepository creates a new PlaceRepository.
|
||||||
@ -18,11 +21,14 @@ func NewPlaceRepository(db *gorm.DB) domain.PlaceRepository {
|
|||||||
return &placeRepository{
|
return &placeRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Place](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Place](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("place.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByCountryID finds places by country ID
|
// ListByCountryID finds places by country ID
|
||||||
func (r *placeRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Place, error) {
|
func (r *placeRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Place, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByCountryID")
|
||||||
|
defer span.End()
|
||||||
var places []domain.Place
|
var places []domain.Place
|
||||||
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&places).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&places).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -32,6 +38,8 @@ func (r *placeRepository) ListByCountryID(ctx context.Context, countryID uint) (
|
|||||||
|
|
||||||
// ListByCityID finds places by city ID
|
// ListByCityID finds places by city ID
|
||||||
func (r *placeRepository) ListByCityID(ctx context.Context, cityID uint) ([]domain.Place, error) {
|
func (r *placeRepository) ListByCityID(ctx context.Context, cityID uint) ([]domain.Place, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByCityID")
|
||||||
|
defer span.End()
|
||||||
var places []domain.Place
|
var places []domain.Place
|
||||||
if err := r.db.WithContext(ctx).Where("city_id = ?", cityID).Find(&places).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("city_id = ?", cityID).Find(&places).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -41,6 +49,8 @@ func (r *placeRepository) ListByCityID(ctx context.Context, cityID uint) ([]doma
|
|||||||
|
|
||||||
// FindNearby finds places within a certain radius (in kilometers) of a point
|
// FindNearby finds places within a certain radius (in kilometers) of a point
|
||||||
func (r *placeRepository) FindNearby(ctx context.Context, latitude, longitude float64, radiusKm float64) ([]domain.Place, error) {
|
func (r *placeRepository) FindNearby(ctx context.Context, latitude, longitude float64, radiusKm float64) ([]domain.Place, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindNearby")
|
||||||
|
defer span.End()
|
||||||
// This is a simplified implementation that would need to be replaced with
|
// This is a simplified implementation that would need to be replaced with
|
||||||
// a proper geospatial query based on the database being used
|
// a proper geospatial query based on the database being used
|
||||||
var places []domain.Place
|
var places []domain.Place
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type publisherRepository struct {
|
type publisherRepository struct {
|
||||||
domain.BaseRepository[domain.Publisher]
|
domain.BaseRepository[domain.Publisher]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPublisherRepository creates a new PublisherRepository.
|
// NewPublisherRepository creates a new PublisherRepository.
|
||||||
@ -17,11 +20,14 @@ func NewPublisherRepository(db *gorm.DB) domain.PublisherRepository {
|
|||||||
return &publisherRepository{
|
return &publisherRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("publisher.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByCountryID finds publishers by country ID
|
// ListByCountryID finds publishers by country ID
|
||||||
func (r *publisherRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Publisher, error) {
|
func (r *publisherRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Publisher, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByCountryID")
|
||||||
|
defer span.End()
|
||||||
var publishers []domain.Publisher
|
var publishers []domain.Publisher
|
||||||
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&publishers).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&publishers).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sourceRepository struct {
|
type sourceRepository struct {
|
||||||
domain.BaseRepository[domain.Source]
|
domain.BaseRepository[domain.Source]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSourceRepository creates a new SourceRepository.
|
// NewSourceRepository creates a new SourceRepository.
|
||||||
@ -18,11 +21,14 @@ func NewSourceRepository(db *gorm.DB) domain.SourceRepository {
|
|||||||
return &sourceRepository{
|
return &sourceRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Source](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Source](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("source.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByWorkID finds sources by work ID
|
// ListByWorkID finds sources by work ID
|
||||||
func (r *sourceRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Source, error) {
|
func (r *sourceRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Source, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var sources []domain.Source
|
var sources []domain.Source
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN work_sources ON work_sources.source_id = sources.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN work_sources ON work_sources.source_id = sources.id").
|
||||||
Where("work_sources.work_id = ?", workID).
|
Where("work_sources.work_id = ?", workID).
|
||||||
@ -34,6 +40,8 @@ func (r *sourceRepository) ListByWorkID(ctx context.Context, workID uint) ([]dom
|
|||||||
|
|
||||||
// FindByURL finds a source by URL
|
// FindByURL finds a source by URL
|
||||||
func (r *sourceRepository) FindByURL(ctx context.Context, url string) (*domain.Source, error) {
|
func (r *sourceRepository) FindByURL(ctx context.Context, url string) (*domain.Source, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByURL")
|
||||||
|
defer span.End()
|
||||||
var source domain.Source
|
var source domain.Source
|
||||||
if err := r.db.WithContext(ctx).Where("url = ?", url).First(&source).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("url = ?", url).First(&source).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tagRepository struct {
|
type tagRepository struct {
|
||||||
domain.BaseRepository[domain.Tag]
|
domain.BaseRepository[domain.Tag]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTagRepository creates a new TagRepository.
|
// NewTagRepository creates a new TagRepository.
|
||||||
@ -18,11 +21,14 @@ func NewTagRepository(db *gorm.DB) domain.TagRepository {
|
|||||||
return &tagRepository{
|
return &tagRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("tag.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByName finds a tag by name
|
// FindByName finds a tag by name
|
||||||
func (r *tagRepository) FindByName(ctx context.Context, name string) (*domain.Tag, error) {
|
func (r *tagRepository) FindByName(ctx context.Context, name string) (*domain.Tag, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByName")
|
||||||
|
defer span.End()
|
||||||
var tag domain.Tag
|
var tag domain.Tag
|
||||||
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&tag).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&tag).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@ -35,6 +41,8 @@ func (r *tagRepository) FindByName(ctx context.Context, name string) (*domain.Ta
|
|||||||
|
|
||||||
// ListByWorkID finds tags by work ID
|
// ListByWorkID finds tags by work ID
|
||||||
func (r *tagRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) {
|
func (r *tagRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var tags []domain.Tag
|
var tags []domain.Tag
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN work_tags ON work_tags.tag_id = tags.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN work_tags ON work_tags.tag_id = tags.id").
|
||||||
Where("work_tags.work_id = ?", workID).
|
Where("work_tags.work_id = ?", workID).
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type translationRepository struct {
|
type translationRepository struct {
|
||||||
domain.BaseRepository[domain.Translation]
|
domain.BaseRepository[domain.Translation]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTranslationRepository creates a new TranslationRepository.
|
// NewTranslationRepository creates a new TranslationRepository.
|
||||||
@ -17,11 +20,14 @@ func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository {
|
|||||||
return &translationRepository{
|
return &translationRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("translation.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByWorkID finds translations by work ID
|
// ListByWorkID finds translations by work ID
|
||||||
func (r *translationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
func (r *translationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
|
defer span.End()
|
||||||
var translations []domain.Translation
|
var translations []domain.Translation
|
||||||
if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", workID, "works").Find(&translations).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", workID, "works").Find(&translations).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -31,6 +37,8 @@ func (r *translationRepository) ListByWorkID(ctx context.Context, workID uint) (
|
|||||||
|
|
||||||
// ListByEntity finds translations by entity type and ID
|
// ListByEntity finds translations by entity type and ID
|
||||||
func (r *translationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
func (r *translationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByEntity")
|
||||||
|
defer span.End()
|
||||||
var translations []domain.Translation
|
var translations []domain.Translation
|
||||||
if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", entityID, entityType).Find(&translations).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("translatable_id = ? AND translatable_type = ?", entityID, entityType).Find(&translations).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -40,6 +48,8 @@ func (r *translationRepository) ListByEntity(ctx context.Context, entityType str
|
|||||||
|
|
||||||
// ListByTranslatorID finds translations by translator ID
|
// ListByTranslatorID finds translations by translator ID
|
||||||
func (r *translationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
func (r *translationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByTranslatorID")
|
||||||
|
defer span.End()
|
||||||
var translations []domain.Translation
|
var translations []domain.Translation
|
||||||
if err := r.db.WithContext(ctx).Where("translator_id = ?", translatorID).Find(&translations).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("translator_id = ?", translatorID).Find(&translations).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -49,6 +59,8 @@ func (r *translationRepository) ListByTranslatorID(ctx context.Context, translat
|
|||||||
|
|
||||||
// ListByStatus finds translations by status
|
// ListByStatus finds translations by status
|
||||||
func (r *translationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
func (r *translationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByStatus")
|
||||||
|
defer span.End()
|
||||||
var translations []domain.Translation
|
var translations []domain.Translation
|
||||||
if err := r.db.WithContext(ctx).Where("status = ?", status).Find(&translations).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("status = ?", status).Find(&translations).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type userProfileRepository struct {
|
type userProfileRepository struct {
|
||||||
domain.BaseRepository[domain.UserProfile]
|
domain.BaseRepository[domain.UserProfile]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserProfileRepository creates a new UserProfileRepository.
|
// NewUserProfileRepository creates a new UserProfileRepository.
|
||||||
@ -18,11 +21,14 @@ func NewUserProfileRepository(db *gorm.DB) domain.UserProfileRepository {
|
|||||||
return &userProfileRepository{
|
return &userProfileRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("user_profile.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByUserID finds a user profile by user ID
|
// GetByUserID finds a user profile by user ID
|
||||||
func (r *userProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) {
|
func (r *userProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByUserID")
|
||||||
|
defer span.End()
|
||||||
var profile domain.UserProfile
|
var profile domain.UserProfile
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&profile).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&profile).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
@ -5,12 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type userRepository struct {
|
type userRepository struct {
|
||||||
domain.BaseRepository[domain.User]
|
domain.BaseRepository[domain.User]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserRepository creates a new UserRepository.
|
// NewUserRepository creates a new UserRepository.
|
||||||
@ -18,11 +21,14 @@ func NewUserRepository(db *gorm.DB) domain.UserRepository {
|
|||||||
return &userRepository{
|
return &userRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.User](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.User](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("user.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByUsername finds a user by username
|
// FindByUsername finds a user by username
|
||||||
func (r *userRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
func (r *userRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByUsername")
|
||||||
|
defer span.End()
|
||||||
var user domain.User
|
var user domain.User
|
||||||
if err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@ -35,6 +41,8 @@ func (r *userRepository) FindByUsername(ctx context.Context, username string) (*
|
|||||||
|
|
||||||
// FindByEmail finds a user by email
|
// FindByEmail finds a user by email
|
||||||
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByEmail")
|
||||||
|
defer span.End()
|
||||||
var user domain.User
|
var user domain.User
|
||||||
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@ -47,6 +55,8 @@ func (r *userRepository) FindByEmail(ctx context.Context, email string) (*domain
|
|||||||
|
|
||||||
// ListByRole lists users by role
|
// ListByRole lists users by role
|
||||||
func (r *userRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
func (r *userRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListByRole")
|
||||||
|
defer span.End()
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
if err := r.db.WithContext(ctx).Where("role = ?", role).Find(&users).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("role = ?", role).Find(&users).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -6,12 +6,15 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type userSessionRepository struct {
|
type userSessionRepository struct {
|
||||||
domain.BaseRepository[domain.UserSession]
|
domain.BaseRepository[domain.UserSession]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserSessionRepository creates a new UserSessionRepository.
|
// NewUserSessionRepository creates a new UserSessionRepository.
|
||||||
@ -19,11 +22,14 @@ func NewUserSessionRepository(db *gorm.DB) domain.UserSessionRepository {
|
|||||||
return &userSessionRepository{
|
return &userSessionRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("user_session.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByToken finds a session by token
|
// GetByToken finds a session by token
|
||||||
func (r *userSessionRepository) GetByToken(ctx context.Context, token string) (*domain.UserSession, error) {
|
func (r *userSessionRepository) GetByToken(ctx context.Context, token string) (*domain.UserSession, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByToken")
|
||||||
|
defer span.End()
|
||||||
var session domain.UserSession
|
var session domain.UserSession
|
||||||
if err := r.db.WithContext(ctx).Where("token = ?", token).First(&session).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("token = ?", token).First(&session).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@ -36,6 +42,8 @@ func (r *userSessionRepository) GetByToken(ctx context.Context, token string) (*
|
|||||||
|
|
||||||
// GetByUserID finds sessions by user ID
|
// GetByUserID finds sessions by user ID
|
||||||
func (r *userSessionRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.UserSession, error) {
|
func (r *userSessionRepository) GetByUserID(ctx context.Context, userID uint) ([]domain.UserSession, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetByUserID")
|
||||||
|
defer span.End()
|
||||||
var sessions []domain.UserSession
|
var sessions []domain.UserSession
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&sessions).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&sessions).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -45,6 +53,8 @@ func (r *userSessionRepository) GetByUserID(ctx context.Context, userID uint) ([
|
|||||||
|
|
||||||
// DeleteExpired deletes expired sessions
|
// DeleteExpired deletes expired sessions
|
||||||
func (r *userSessionRepository) DeleteExpired(ctx context.Context) error {
|
func (r *userSessionRepository) DeleteExpired(ctx context.Context) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "DeleteExpired")
|
||||||
|
defer span.End()
|
||||||
if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.UserSession{}).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&domain.UserSession{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,15 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/work"
|
"tercul/internal/domain/work"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type workRepository struct {
|
type workRepository struct {
|
||||||
domain.BaseRepository[work.Work]
|
domain.BaseRepository[work.Work]
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
tracer trace.Tracer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkRepository creates a new WorkRepository.
|
// NewWorkRepository creates a new WorkRepository.
|
||||||
@ -20,11 +23,14 @@ func NewWorkRepository(db *gorm.DB) work.WorkRepository {
|
|||||||
return &workRepository{
|
return &workRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[work.Work](db),
|
BaseRepository: NewBaseRepositoryImpl[work.Work](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
tracer: otel.Tracer("work.repository"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByTitle finds works by title (partial match)
|
// FindByTitle finds works by title (partial match)
|
||||||
func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]work.Work, error) {
|
func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]work.Work, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByTitle")
|
||||||
|
defer span.End()
|
||||||
var works []work.Work
|
var works []work.Work
|
||||||
if err := r.db.WithContext(ctx).Where("title LIKE ?", "%"+title+"%").Find(&works).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("title LIKE ?", "%"+title+"%").Find(&works).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -34,6 +40,8 @@ func (r *workRepository) FindByTitle(ctx context.Context, title string) ([]work.
|
|||||||
|
|
||||||
// FindByAuthor finds works by author ID
|
// FindByAuthor finds works by author ID
|
||||||
func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
|
func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]work.Work, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByAuthor")
|
||||||
|
defer span.End()
|
||||||
var works []work.Work
|
var works []work.Work
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.work_id = works.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.work_id = works.id").
|
||||||
Where("work_authors.author_id = ?", authorID).
|
Where("work_authors.author_id = ?", authorID).
|
||||||
@ -45,6 +53,8 @@ func (r *workRepository) FindByAuthor(ctx context.Context, authorID uint) ([]wor
|
|||||||
|
|
||||||
// FindByCategory finds works by category ID
|
// FindByCategory finds works by category ID
|
||||||
func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
|
func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([]work.Work, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByCategory")
|
||||||
|
defer span.End()
|
||||||
var works []work.Work
|
var works []work.Work
|
||||||
if err := r.db.WithContext(ctx).Joins("JOIN work_categories ON work_categories.work_id = works.id").
|
if err := r.db.WithContext(ctx).Joins("JOIN work_categories ON work_categories.work_id = works.id").
|
||||||
Where("work_categories.category_id = ?", categoryID).
|
Where("work_categories.category_id = ?", categoryID).
|
||||||
@ -56,6 +66,8 @@ func (r *workRepository) FindByCategory(ctx context.Context, categoryID uint) ([
|
|||||||
|
|
||||||
// FindByLanguage finds works by language with pagination
|
// FindByLanguage finds works by language with pagination
|
||||||
func (r *workRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
func (r *workRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "FindByLanguage")
|
||||||
|
defer span.End()
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
@ -104,6 +116,8 @@ func (r *workRepository) FindByLanguage(ctx context.Context, language string, pa
|
|||||||
|
|
||||||
// Delete removes a work and its associations
|
// Delete removes a work and its associations
|
||||||
func (r *workRepository) Delete(ctx context.Context, id uint) error {
|
func (r *workRepository) Delete(ctx context.Context, id uint) error {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "Delete")
|
||||||
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Manually delete associations
|
// Manually delete associations
|
||||||
if err := tx.Select("Copyrights", "Monetizations", "Authors", "Tags", "Categories").Delete(&work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: id}}}).Error; err != nil {
|
if err := tx.Select("Copyrights", "Monetizations", "Authors", "Tags", "Categories").Delete(&work.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: id}}}).Error; err != nil {
|
||||||
@ -119,11 +133,15 @@ func (r *workRepository) Delete(ctx context.Context, id uint) error {
|
|||||||
|
|
||||||
// GetWithTranslations gets a work with its translations
|
// GetWithTranslations gets a work with its translations
|
||||||
func (r *workRepository) GetWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
|
func (r *workRepository) GetWithTranslations(ctx context.Context, id uint) (*work.Work, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetWithTranslations")
|
||||||
|
defer span.End()
|
||||||
return r.FindWithPreload(ctx, []string{"Translations"}, id)
|
return r.FindWithPreload(ctx, []string{"Translations"}, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWithAssociations gets a work with all of its direct and many-to-many associations.
|
// GetWithAssociations gets a work with all of its direct and many-to-many associations.
|
||||||
func (r *workRepository) GetWithAssociations(ctx context.Context, id uint) (*work.Work, error) {
|
func (r *workRepository) GetWithAssociations(ctx context.Context, id uint) (*work.Work, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetWithAssociations")
|
||||||
|
defer span.End()
|
||||||
associations := []string{
|
associations := []string{
|
||||||
"Translations",
|
"Translations",
|
||||||
"Authors",
|
"Authors",
|
||||||
@ -137,6 +155,8 @@ func (r *workRepository) GetWithAssociations(ctx context.Context, id uint) (*wor
|
|||||||
|
|
||||||
// GetWithAssociationsInTx gets a work with all associations within a transaction.
|
// GetWithAssociationsInTx gets a work with all associations within a transaction.
|
||||||
func (r *workRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*work.Work, error) {
|
func (r *workRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*work.Work, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "GetWithAssociationsInTx")
|
||||||
|
defer span.End()
|
||||||
var entity work.Work
|
var entity work.Work
|
||||||
query := tx.WithContext(ctx)
|
query := tx.WithContext(ctx)
|
||||||
associations := []string{
|
associations := []string{
|
||||||
@ -163,6 +183,8 @@ func (r *workRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.D
|
|||||||
// Note: This assumes a direct relationship between user ID and author ID,
|
// Note: This assumes a direct relationship between user ID and author ID,
|
||||||
// which may need to be revised based on the actual domain model.
|
// which may need to be revised based on the actual domain model.
|
||||||
func (r *workRepository) IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
func (r *workRepository) IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "IsAuthor")
|
||||||
|
defer span.End()
|
||||||
var count int64
|
var count int64
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Table("work_authors").
|
Table("work_authors").
|
||||||
@ -176,6 +198,8 @@ func (r *workRepository) IsAuthor(ctx context.Context, workID uint, authorID uin
|
|||||||
|
|
||||||
// ListWithTranslations lists works with their translations
|
// ListWithTranslations lists works with their translations
|
||||||
func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
func (r *workRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[work.Work], error) {
|
||||||
|
ctx, span := r.tracer.Start(ctx, "ListWithTranslations")
|
||||||
|
defer span.End()
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
|
|||||||
@ -195,6 +195,7 @@ type Author struct {
|
|||||||
Status AuthorStatus `gorm:"size:50;default:'active'"`
|
Status AuthorStatus `gorm:"size:50;default:'active'"`
|
||||||
BirthDate *time.Time
|
BirthDate *time.Time
|
||||||
DeathDate *time.Time
|
DeathDate *time.Time
|
||||||
|
OpenLibraryID string `gorm:"size:50;index"`
|
||||||
Books []*Book `gorm:"many2many:book_authors"`
|
Books []*Book `gorm:"many2many:book_authors"`
|
||||||
CountryID *uint
|
CountryID *uint
|
||||||
Country *Country `gorm:"foreignKey:CountryID"`
|
Country *Country `gorm:"foreignKey:CountryID"`
|
||||||
|
|||||||
@ -2,19 +2,16 @@ package domain
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
|
// Common domain-level errors that can be used across repositories and services.
|
||||||
var (
|
var (
|
||||||
// ErrNotFound indicates that a requested resource was not found.
|
ErrEntityNotFound = errors.New("entity not found")
|
||||||
ErrNotFound = errors.New("not found")
|
ErrInvalidID = errors.New("invalid ID: cannot be zero")
|
||||||
|
ErrInvalidInput = errors.New("invalid input parameters")
|
||||||
// ErrUnauthorized indicates that the user is not authenticated.
|
ErrDatabaseOperation = errors.New("database operation failed")
|
||||||
ErrUnauthorized = errors.New("unauthorized")
|
ErrContextRequired = errors.New("context is required")
|
||||||
|
ErrTransactionFailed = errors.New("transaction failed")
|
||||||
// ErrForbidden indicates that the user is authenticated but not authorized to perform the action.
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
ErrForbidden = errors.New("forbidden")
|
ErrForbidden = errors.New("forbidden")
|
||||||
|
ErrValidation = errors.New("validation failed")
|
||||||
// ErrValidation indicates that the input failed validation.
|
ErrConflict = errors.New("conflict with existing resource")
|
||||||
ErrValidation = errors.New("validation failed")
|
|
||||||
|
|
||||||
// ErrConflict indicates a conflict with the current state of the resource (e.g., duplicate).
|
|
||||||
ErrConflict = errors.New("conflict")
|
|
||||||
)
|
)
|
||||||
@ -9,4 +9,5 @@ type LocalizationRepository interface {
|
|||||||
GetTranslation(ctx context.Context, key string, language string) (string, error)
|
GetTranslation(ctx context.Context, key string, language string) (string, error)
|
||||||
GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error)
|
GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error)
|
||||||
GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error)
|
GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error)
|
||||||
|
GetWorkContent(ctx context.Context, workID uint, language string) (string, error)
|
||||||
}
|
}
|
||||||
78
internal/enrichment/author_enricher.go
Normal file
78
internal/enrichment/author_enricher.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package enrichment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/openlibrary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenLibraryAuthorEnricher enriches author data from the Open Library API.
|
||||||
|
type OpenLibraryAuthorEnricher struct {
|
||||||
|
client *openlibrary.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOpenLibraryAuthorEnricher creates a new OpenLibraryAuthorEnricher.
|
||||||
|
func NewOpenLibraryAuthorEnricher() *OpenLibraryAuthorEnricher {
|
||||||
|
return &OpenLibraryAuthorEnricher{
|
||||||
|
client: openlibrary.NewClient(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the enricher.
|
||||||
|
func (e *OpenLibraryAuthorEnricher) Name() string {
|
||||||
|
return "openlibrary_author_enricher"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich fetches data from the Open Library API and enriches the author.
|
||||||
|
func (e *OpenLibraryAuthorEnricher) Enrich(ctx context.Context, author *domain.Author) error {
|
||||||
|
if author.OpenLibraryID == "" {
|
||||||
|
// No OLID to look up.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
olAuthor, err := e.client.GetAuthor(ctx, author.OpenLibraryID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get author from Open Library: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if olAuthor.Bio != nil {
|
||||||
|
// The bio can be a string or a struct with a 'value' field.
|
||||||
|
if bioStr, ok := olAuthor.Bio.(string); ok {
|
||||||
|
// Find or create the English translation for the bio.
|
||||||
|
e.updateBioTranslation(author, bioStr)
|
||||||
|
} else if bioMap, ok := olAuthor.Bio.(map[string]interface{}); ok {
|
||||||
|
if bioValue, ok := bioMap["value"].(string); ok {
|
||||||
|
e.updateBioTranslation(author, bioValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *OpenLibraryAuthorEnricher) updateBioTranslation(author *domain.Author, bio string) {
|
||||||
|
// This is a simplified implementation. A real one would need to handle
|
||||||
|
// creating or updating a translation record associated with the author.
|
||||||
|
// For now, we'll just append it to the author's existing bio if it's empty.
|
||||||
|
var bioTranslation *domain.Translation
|
||||||
|
for _, t := range author.Translations {
|
||||||
|
if t.TranslatableType == "authors" && t.Language == "en" {
|
||||||
|
bioTranslation = t
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bioTranslation == nil {
|
||||||
|
author.Translations = append(author.Translations, &domain.Translation{
|
||||||
|
Content: bio,
|
||||||
|
Language: "en",
|
||||||
|
TranslatableID: author.ID,
|
||||||
|
TranslatableType: "authors",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if bioTranslation.Content == "" {
|
||||||
|
bioTranslation.Content = bio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
internal/enrichment/service.go
Normal file
67
internal/enrichment/service.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package enrichment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/domain/work"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service is the main entrypoint for the enrichment functionality.
|
||||||
|
// It orchestrates different enrichers for various domain entities.
|
||||||
|
type Service struct {
|
||||||
|
AuthorEnrichers []AuthorEnricher
|
||||||
|
WorkEnrichers []WorkEnricher
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new enrichment Service.
|
||||||
|
func NewService() *Service {
|
||||||
|
service := &Service{
|
||||||
|
AuthorEnrichers: []AuthorEnricher{},
|
||||||
|
WorkEnrichers: []WorkEnricher{},
|
||||||
|
}
|
||||||
|
service.RegisterAuthorEnricher(NewOpenLibraryAuthorEnricher())
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorEnricher defines the interface for enriching author data.
|
||||||
|
type AuthorEnricher interface {
|
||||||
|
Enrich(ctx context.Context, author *domain.Author) error
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkEnricher defines the interface for enriching work data.
|
||||||
|
type WorkEnricher interface {
|
||||||
|
Enrich(ctx context.Context, work *work.Work) error
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterAuthorEnricher adds a new author enricher to the service.
|
||||||
|
func (s *Service) RegisterAuthorEnricher(enricher AuthorEnricher) {
|
||||||
|
s.AuthorEnrichers = append(s.AuthorEnrichers, enricher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterWorkEnricher adds a new work enricher to the service.
|
||||||
|
func (s *Service) RegisterWorkEnricher(enricher WorkEnricher) {
|
||||||
|
s.WorkEnrichers = append(s.WorkEnrichers, enricher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnrichAuthor iterates through registered author enrichers and applies them.
|
||||||
|
func (s *Service) EnrichAuthor(ctx context.Context, author *domain.Author) error {
|
||||||
|
for _, enricher := range s.AuthorEnrichers {
|
||||||
|
if err := enricher.Enrich(ctx, author); err != nil {
|
||||||
|
// In a real implementation, we might want to log errors but continue.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnrichWork iterates through registered work enrichers and applies them.
|
||||||
|
func (s *Service) EnrichWork(ctx context.Context, work *work.Work) error {
|
||||||
|
for _, enricher := range s.WorkEnrichers {
|
||||||
|
if err := enricher.Enrich(ctx, work); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -117,9 +117,7 @@ func (c *RedisAnalysisCache) Set(ctx context.Context, key string, result *Analys
|
|||||||
ttlSeconds := config.Cfg.NLPRedisCacheTTLSeconds
|
ttlSeconds := config.Cfg.NLPRedisCacheTTLSeconds
|
||||||
err := c.cache.Set(ctx, key, result, time.Duration(ttlSeconds)*time.Second)
|
err := c.cache.Set(ctx, key, result, time.Duration(ttlSeconds)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Failed to cache analysis result",
|
log.FromContext(ctx).With("key", key).Error(err, "Failed to cache analysis result")
|
||||||
log.F("key", key),
|
|
||||||
log.F("error", err))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,16 +174,12 @@ func (c *CompositeAnalysisCache) Set(ctx context.Context, key string, result *An
|
|||||||
|
|
||||||
// Set in memory cache
|
// Set in memory cache
|
||||||
if err := c.memoryCache.Set(ctx, key, result); err != nil {
|
if err := c.memoryCache.Set(ctx, key, result); err != nil {
|
||||||
log.LogWarn("Failed to set memory cache",
|
log.FromContext(ctx).With("key", key).Error(err, "Failed to set memory cache")
|
||||||
log.F("key", key),
|
|
||||||
log.F("error", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set in Redis cache
|
// Set in Redis cache
|
||||||
if err := c.redisCache.Set(ctx, key, result); err != nil {
|
if err := c.redisCache.Set(ctx, key, result); err != nil {
|
||||||
log.LogWarn("Failed to set Redis cache",
|
log.FromContext(ctx).With("key", key).Error(err, "Failed to set Redis cache")
|
||||||
log.F("key", key),
|
|
||||||
log.F("error", err))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,7 @@ func NewGORMAnalysisRepository(db *gorm.DB) *GORMAnalysisRepository {
|
|||||||
|
|
||||||
// StoreAnalysisResults stores analysis results in the database
|
// StoreAnalysisResults stores analysis results in the database
|
||||||
func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workID uint, result *AnalysisResult) error {
|
func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workID uint, result *AnalysisResult) error {
|
||||||
|
logger := log.FromContext(ctx).With("workID", workID)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
return fmt.Errorf("analysis result cannot be nil")
|
return fmt.Errorf("analysis result cannot be nil")
|
||||||
}
|
}
|
||||||
@ -48,9 +49,7 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI
|
|||||||
// Determine language from the work record to avoid hardcoded defaults
|
// Determine language from the work record to avoid hardcoded defaults
|
||||||
var workRecord work.Work
|
var workRecord work.Work
|
||||||
if err := r.db.WithContext(ctx).First(&workRecord, workID).Error; err != nil {
|
if err := r.db.WithContext(ctx).First(&workRecord, workID).Error; err != nil {
|
||||||
log.LogError("Failed to fetch work for language",
|
logger.Error(err, "Failed to fetch work for language")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to fetch work for language: %w", err)
|
return fmt.Errorf("failed to fetch work for language: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,12 +88,11 @@ func (r *GORMAnalysisRepository) StoreAnalysisResults(ctx context.Context, workI
|
|||||||
|
|
||||||
// GetWorkContent retrieves content for a work from translations
|
// GetWorkContent retrieves content for a work from translations
|
||||||
func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
||||||
|
logger := log.FromContext(ctx).With("workID", workID)
|
||||||
// First, get the work to determine its language
|
// First, get the work to determine its language
|
||||||
var workRecord work.Work
|
var workRecord work.Work
|
||||||
if err := r.db.First(&workRecord, workID).Error; err != nil {
|
if err := r.db.First(&workRecord, workID).Error; err != nil {
|
||||||
log.LogError("Failed to fetch work for content retrieval",
|
logger.Error(err, "Failed to fetch work for content retrieval")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return "", fmt.Errorf("failed to fetch work: %w", err)
|
return "", fmt.Errorf("failed to fetch work: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,19 +105,19 @@ func (r *GORMAnalysisRepository) GetWorkContent(ctx context.Context, workID uint
|
|||||||
|
|
||||||
// Try original language first
|
// Try original language first
|
||||||
if err := r.db.Where("translatable_type = ? AND translatable_id = ? AND is_original_language = ?",
|
if err := r.db.Where("translatable_type = ? AND translatable_id = ? AND is_original_language = ?",
|
||||||
"Work", workID, true).First(&translation).Error; err == nil {
|
"works", workID, true).First(&translation).Error; err == nil {
|
||||||
return translation.Content, nil
|
return translation.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try work's language
|
// Try work's language
|
||||||
if err := r.db.Where("translatable_type = ? AND translatable_id = ? AND language = ?",
|
if err := r.db.Where("translatable_type = ? AND translatable_id = ? AND language = ?",
|
||||||
"Work", workID, workRecord.Language).First(&translation).Error; err == nil {
|
"works", workID, workRecord.Language).First(&translation).Error; err == nil {
|
||||||
return translation.Content, nil
|
return translation.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try any available translation
|
// Try any available translation
|
||||||
if err := r.db.Where("translatable_type = ? AND translatable_id = ?",
|
if err := r.db.Where("translatable_type = ? AND translatable_id = ?",
|
||||||
"Work", workID).First(&translation).Error; err == nil {
|
"works", workID).First(&translation).Error; err == nil {
|
||||||
return translation.Content, nil
|
return translation.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,23 +135,21 @@ func (r *GORMAnalysisRepository) GetWorkByID(ctx context.Context, workID uint) (
|
|||||||
|
|
||||||
// GetAnalysisData fetches persisted analysis data for a work
|
// GetAnalysisData fetches persisted analysis data for a work
|
||||||
func (r *GORMAnalysisRepository) GetAnalysisData(ctx context.Context, workID uint) (*domain.TextMetadata, *domain.ReadabilityScore, *domain.LanguageAnalysis, error) {
|
func (r *GORMAnalysisRepository) GetAnalysisData(ctx context.Context, workID uint) (*domain.TextMetadata, *domain.ReadabilityScore, *domain.LanguageAnalysis, error) {
|
||||||
|
logger := log.FromContext(ctx).With("workID", workID)
|
||||||
var textMetadata domain.TextMetadata
|
var textMetadata domain.TextMetadata
|
||||||
var readabilityScore domain.ReadabilityScore
|
var readabilityScore domain.ReadabilityScore
|
||||||
var languageAnalysis domain.LanguageAnalysis
|
var languageAnalysis domain.LanguageAnalysis
|
||||||
|
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&textMetadata).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&textMetadata).Error; err != nil {
|
||||||
log.LogWarn("No text metadata found for work",
|
logger.Warn("No text metadata found for work")
|
||||||
log.F("workID", workID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&readabilityScore).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&readabilityScore).Error; err != nil {
|
||||||
log.LogWarn("No readability score found for work",
|
logger.Warn("No readability score found for work")
|
||||||
log.F("workID", workID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&languageAnalysis).Error; err != nil {
|
if err := r.db.WithContext(ctx).Where("work_id = ?", workID).First(&languageAnalysis).Error; err != nil {
|
||||||
log.LogWarn("No language analysis found for work",
|
logger.Warn("No language analysis found for work")
|
||||||
log.F("workID", workID))
|
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,22 +160,18 @@ func (r *GORMAnalysisRepository) GetAnalysisData(ctx context.Context, workID uin
|
|||||||
func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID uint,
|
func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID uint,
|
||||||
textMetadata *domain.TextMetadata, readabilityScore *domain.ReadabilityScore,
|
textMetadata *domain.TextMetadata, readabilityScore *domain.ReadabilityScore,
|
||||||
languageAnalysis *domain.LanguageAnalysis) error {
|
languageAnalysis *domain.LanguageAnalysis) error {
|
||||||
|
logger := log.FromContext(ctx).With("workID", workID)
|
||||||
// Use a transaction to ensure all data is stored atomically
|
// Use a transaction to ensure all data is stored atomically
|
||||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
// Store text metadata
|
// Store text metadata
|
||||||
if textMetadata != nil {
|
if textMetadata != nil {
|
||||||
if err := tx.Where("work_id = ?", workID).Delete(&domain.TextMetadata{}).Error; err != nil {
|
if err := tx.Where("work_id = ?", workID).Delete(&domain.TextMetadata{}).Error; err != nil {
|
||||||
log.LogError("Failed to delete existing text metadata",
|
logger.Error(err, "Failed to delete existing text metadata")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to delete existing text metadata: %w", err)
|
return fmt.Errorf("failed to delete existing text metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(textMetadata).Error; err != nil {
|
if err := tx.Create(textMetadata).Error; err != nil {
|
||||||
log.LogError("Failed to store text metadata",
|
logger.Error(err, "Failed to store text metadata")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to store text metadata: %w", err)
|
return fmt.Errorf("failed to store text metadata: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,16 +179,12 @@ func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID u
|
|||||||
// Store readability score
|
// Store readability score
|
||||||
if readabilityScore != nil {
|
if readabilityScore != nil {
|
||||||
if err := tx.Where("work_id = ?", workID).Delete(&domain.ReadabilityScore{}).Error; err != nil {
|
if err := tx.Where("work_id = ?", workID).Delete(&domain.ReadabilityScore{}).Error; err != nil {
|
||||||
log.LogError("Failed to delete existing readability score",
|
logger.Error(err, "Failed to delete existing readability score")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to delete existing readability score: %w", err)
|
return fmt.Errorf("failed to delete existing readability score: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(readabilityScore).Error; err != nil {
|
if err := tx.Create(readabilityScore).Error; err != nil {
|
||||||
log.LogError("Failed to store readability score",
|
logger.Error(err, "Failed to store readability score")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to store readability score: %w", err)
|
return fmt.Errorf("failed to store readability score: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,22 +192,17 @@ func (r *GORMAnalysisRepository) StoreWorkAnalysis(ctx context.Context, workID u
|
|||||||
// Store language analysis
|
// Store language analysis
|
||||||
if languageAnalysis != nil {
|
if languageAnalysis != nil {
|
||||||
if err := tx.Where("work_id = ?", workID).Delete(&domain.LanguageAnalysis{}).Error; err != nil {
|
if err := tx.Where("work_id = ?", workID).Delete(&domain.LanguageAnalysis{}).Error; err != nil {
|
||||||
log.LogError("Failed to delete existing language analysis",
|
logger.Error(err, "Failed to delete existing language analysis")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to delete existing language analysis: %w", err)
|
return fmt.Errorf("failed to delete existing language analysis: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(languageAnalysis).Error; err != nil {
|
if err := tx.Create(languageAnalysis).Error; err != nil {
|
||||||
log.LogError("Failed to store language analysis",
|
logger.Error(err, "Failed to store language analysis")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to store language analysis: %w", err)
|
return fmt.Errorf("failed to store language analysis: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("Successfully stored analysis results",
|
logger.Info("Successfully stored analysis results")
|
||||||
log.F("workID", workID))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@ -79,6 +79,7 @@ func (a *BasicAnalyzer) DisableCache() {
|
|||||||
|
|
||||||
// AnalyzeText performs basic linguistic analysis on the given text
|
// AnalyzeText performs basic linguistic analysis on the given text
|
||||||
func (a *BasicAnalyzer) AnalyzeText(ctx context.Context, text string, language string) (*AnalysisResult, error) {
|
func (a *BasicAnalyzer) AnalyzeText(ctx context.Context, text string, language string) (*AnalysisResult, error) {
|
||||||
|
logger := log.FromContext(ctx).With("language", language).With("textLength", len(text))
|
||||||
// Check in-memory cache first if enabled
|
// Check in-memory cache first if enabled
|
||||||
if a.cacheEnabled {
|
if a.cacheEnabled {
|
||||||
cacheKey := makeTextCacheKey(language, text)
|
cacheKey := makeTextCacheKey(language, text)
|
||||||
@ -89,9 +90,7 @@ func (a *BasicAnalyzer) AnalyzeText(ctx context.Context, text string, language s
|
|||||||
a.cacheMutex.RUnlock()
|
a.cacheMutex.RUnlock()
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
log.LogDebug("In-memory cache hit for text analysis",
|
logger.Debug("In-memory cache hit for text analysis")
|
||||||
log.F("language", language),
|
|
||||||
log.F("textLength", len(text)))
|
|
||||||
return cachedResult, nil
|
return cachedResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,9 +99,7 @@ func (a *BasicAnalyzer) AnalyzeText(ctx context.Context, text string, language s
|
|||||||
var cachedResult AnalysisResult
|
var cachedResult AnalysisResult
|
||||||
err := a.cache.Get(ctx, "text_analysis:"+cacheKey, &cachedResult)
|
err := a.cache.Get(ctx, "text_analysis:"+cacheKey, &cachedResult)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.LogDebug("Redis cache hit for text analysis",
|
logger.Debug("Redis cache hit for text analysis")
|
||||||
log.F("language", language),
|
|
||||||
log.F("textLength", len(text)))
|
|
||||||
|
|
||||||
// Store in in-memory cache too
|
// Store in in-memory cache too
|
||||||
a.cacheMutex.Lock()
|
a.cacheMutex.Lock()
|
||||||
@ -115,9 +112,7 @@ func (a *BasicAnalyzer) AnalyzeText(ctx context.Context, text string, language s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache miss or caching disabled, perform analysis using the pure TextAnalyzer
|
// Cache miss or caching disabled, perform analysis using the pure TextAnalyzer
|
||||||
log.LogDebug("Performing text analysis",
|
logger.Debug("Performing text analysis")
|
||||||
log.F("language", language),
|
|
||||||
log.F("textLength", len(text)))
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
result *AnalysisResult
|
result *AnalysisResult
|
||||||
@ -144,10 +139,7 @@ func (a *BasicAnalyzer) AnalyzeText(ctx context.Context, text string, language s
|
|||||||
// Store in Redis cache if available
|
// Store in Redis cache if available
|
||||||
if a.cache != nil {
|
if a.cache != nil {
|
||||||
if err := a.cache.Set(ctx, "text_analysis:"+cacheKey, result, 0); err != nil {
|
if err := a.cache.Set(ctx, "text_analysis:"+cacheKey, result, 0); err != nil {
|
||||||
log.LogWarn("Failed to cache text analysis result",
|
logger.Error(err, "Failed to cache text analysis result")
|
||||||
log.F("language", language),
|
|
||||||
log.F("textLength", len(text)),
|
|
||||||
log.F("error", err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,8 @@ func NewWorkAnalysisService(
|
|||||||
|
|
||||||
// AnalyzeWork performs linguistic analysis on a work and stores the results
|
// AnalyzeWork performs linguistic analysis on a work and stores the results
|
||||||
func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) error {
|
func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) error {
|
||||||
|
logger := log.FromContext(ctx).With("workID", workID)
|
||||||
|
|
||||||
if workID == 0 {
|
if workID == 0 {
|
||||||
return fmt.Errorf("invalid work ID")
|
return fmt.Errorf("invalid work ID")
|
||||||
}
|
}
|
||||||
@ -77,8 +79,7 @@ func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) erro
|
|||||||
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
|
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
|
||||||
|
|
||||||
if result, err := s.analysisCache.Get(ctx, cacheKey); err == nil {
|
if result, err := s.analysisCache.Get(ctx, cacheKey); err == nil {
|
||||||
log.LogInfo("Cache hit for work analysis",
|
logger.Info("Cache hit for work analysis")
|
||||||
log.F("workID", workID))
|
|
||||||
|
|
||||||
// Store directly to database
|
// Store directly to database
|
||||||
return s.analysisRepo.StoreAnalysisResults(ctx, workID, result)
|
return s.analysisRepo.StoreAnalysisResults(ctx, workID, result)
|
||||||
@ -88,34 +89,28 @@ func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) erro
|
|||||||
// Get work content from database
|
// Get work content from database
|
||||||
content, err := s.analysisRepo.GetWorkContent(ctx, workID, "")
|
content, err := s.analysisRepo.GetWorkContent(ctx, workID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to get work content for analysis",
|
logger.Error(err, "Failed to get work content for analysis")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to get work content: %w", err)
|
return fmt.Errorf("failed to get work content: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip analysis if content is empty
|
// Skip analysis if content is empty
|
||||||
if content == "" {
|
if content == "" {
|
||||||
log.LogWarn("Skipping analysis for work with empty content",
|
logger.Warn("Skipping analysis for work with empty content")
|
||||||
log.F("workID", workID))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get work to determine language (via repository to avoid leaking GORM)
|
// Get work to determine language (via repository to avoid leaking GORM)
|
||||||
work, err := s.analysisRepo.GetWorkByID(ctx, workID)
|
work, err := s.analysisRepo.GetWorkByID(ctx, workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to fetch work for analysis",
|
logger.Error(err, "Failed to fetch work for analysis")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to fetch work: %w", err)
|
return fmt.Errorf("failed to fetch work: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analyze the text
|
// Analyze the text
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
log.LogInfo("Analyzing work",
|
logger.With("language", work.Language).
|
||||||
log.F("workID", workID),
|
With("contentLength", len(content)).
|
||||||
log.F("language", work.Language),
|
Info("Analyzing work")
|
||||||
log.F("contentLength", len(content)))
|
|
||||||
|
|
||||||
var result *AnalysisResult
|
var result *AnalysisResult
|
||||||
|
|
||||||
@ -127,17 +122,13 @@ func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogError("Failed to analyze work text",
|
logger.Error(err, "Failed to analyze work text")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to analyze work text: %w", err)
|
return fmt.Errorf("failed to analyze work text: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store results in database
|
// Store results in database
|
||||||
if err := s.analysisRepo.StoreAnalysisResults(ctx, workID, result); err != nil {
|
if err := s.analysisRepo.StoreAnalysisResults(ctx, workID, result); err != nil {
|
||||||
log.LogError("Failed to store analysis results",
|
logger.Error(err, "Failed to store analysis results")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
return fmt.Errorf("failed to store analysis results: %w", err)
|
return fmt.Errorf("failed to store analysis results: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,18 +136,15 @@ func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) erro
|
|||||||
if s.cacheEnabled && s.analysisCache.IsEnabled() {
|
if s.cacheEnabled && s.analysisCache.IsEnabled() {
|
||||||
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
|
cacheKey := fmt.Sprintf("work_analysis:%d", workID)
|
||||||
if err := s.analysisCache.Set(ctx, cacheKey, result); err != nil {
|
if err := s.analysisCache.Set(ctx, cacheKey, result); err != nil {
|
||||||
log.LogWarn("Failed to cache work analysis result",
|
logger.Error(err, "Failed to cache work analysis result")
|
||||||
log.F("workID", workID),
|
|
||||||
log.F("error", err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("Successfully analyzed work",
|
logger.With("wordCount", result.WordCount).
|
||||||
log.F("workID", workID),
|
With("readabilityScore", result.ReadabilityScore).
|
||||||
log.F("wordCount", result.WordCount),
|
With("sentiment", result.Sentiment).
|
||||||
log.F("readabilityScore", result.ReadabilityScore),
|
With("durationMs", time.Since(start).Milliseconds()).
|
||||||
log.F("sentiment", result.Sentiment),
|
Info("Successfully analyzed work")
|
||||||
log.F("durationMs", time.Since(start).Milliseconds()))
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,3 +52,9 @@ func (l *Logger) Ctx(ctx context.Context) *Logger {
|
|||||||
// `log` is now the correct *zerolog.Logger, so we wrap it.
|
// `log` is now the correct *zerolog.Logger, so we wrap it.
|
||||||
return &Logger{log}
|
return &Logger{log}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With adds a key-value pair to the logger's context.
|
||||||
|
func (l *Logger) With(key string, value interface{}) *Logger {
|
||||||
|
newLogger := l.Logger.With().Interface(key, value).Logger()
|
||||||
|
return &Logger{&newLogger}
|
||||||
|
}
|
||||||
@ -11,8 +11,10 @@ import (
|
|||||||
|
|
||||||
// Metrics contains the Prometheus metrics for the application.
|
// Metrics contains the Prometheus metrics for the application.
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
RequestsTotal *prometheus.CounterVec
|
RequestsTotal *prometheus.CounterVec
|
||||||
RequestDuration *prometheus.HistogramVec
|
RequestDuration *prometheus.HistogramVec
|
||||||
|
DBQueriesTotal *prometheus.CounterVec
|
||||||
|
DBQueryDuration *prometheus.HistogramVec
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMetrics creates and registers the Prometheus metrics.
|
// NewMetrics creates and registers the Prometheus metrics.
|
||||||
@ -33,6 +35,21 @@ func NewMetrics(reg prometheus.Registerer) *Metrics {
|
|||||||
},
|
},
|
||||||
[]string{"method", "path"},
|
[]string{"method", "path"},
|
||||||
),
|
),
|
||||||
|
DBQueriesTotal: promauto.With(reg).NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "db_queries_total",
|
||||||
|
Help: "Total number of database queries.",
|
||||||
|
},
|
||||||
|
[]string{"operation", "status"},
|
||||||
|
),
|
||||||
|
DBQueryDuration: promauto.With(reg).NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "db_query_duration_seconds",
|
||||||
|
Help: "Duration of database queries.",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
[]string{"operation", "status"},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,9 +12,15 @@ import (
|
|||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
// ContextKey is the type for context keys to avoid collisions.
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
const RequestIDKey contextKey = "request_id"
|
const (
|
||||||
|
// RequestIDKey is the key for the request ID in the context.
|
||||||
|
RequestIDKey ContextKey = "request_id"
|
||||||
|
// LoggerContextKey is the key for the logger in the context.
|
||||||
|
LoggerContextKey ContextKey = "logger"
|
||||||
|
)
|
||||||
|
|
||||||
// responseWriter is a wrapper around http.ResponseWriter to capture the status code.
|
// responseWriter is a wrapper around http.ResponseWriter to capture the status code.
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
@ -37,6 +43,35 @@ func RequestIDMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoggingMiddleware creates a request-scoped logger and injects it into the context.
|
||||||
|
func LoggingMiddleware(log *Logger) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Start with a logger that has trace and span IDs.
|
||||||
|
requestLogger := log.Ctx(r.Context())
|
||||||
|
|
||||||
|
// Add request_id to logger context.
|
||||||
|
if reqID, ok := r.Context().Value(RequestIDKey).(string); ok {
|
||||||
|
requestLogger = requestLogger.With("request_id", reqID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the logger to the context.
|
||||||
|
ctx := context.WithValue(r.Context(), LoggerContextKey, requestLogger)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerFromContext retrieves the request-scoped logger from the context.
|
||||||
|
// If no logger is found, it returns a default logger.
|
||||||
|
func LoggerFromContext(ctx context.Context) *Logger {
|
||||||
|
if logger, ok := ctx.Value(LoggerContextKey).(*Logger); ok {
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
// Fallback to a default logger if none is found in context.
|
||||||
|
return NewLogger("tercul-fallback", "development")
|
||||||
|
}
|
||||||
|
|
||||||
// TracingMiddleware creates a new OpenTelemetry span for each request.
|
// TracingMiddleware creates a new OpenTelemetry span for each request.
|
||||||
func TracingMiddleware(next http.Handler) http.Handler {
|
func TracingMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"tercul/internal/observability"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ const (
|
|||||||
func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger := log.FromContext(r.Context())
|
||||||
// Skip authentication for certain paths
|
// Skip authentication for certain paths
|
||||||
if shouldSkipAuth(r.URL.Path) {
|
if shouldSkipAuth(r.URL.Path) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@ -32,9 +33,7 @@ func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
|||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Authentication failed - missing or invalid token",
|
logger.Warn("Authentication failed - missing or invalid token")
|
||||||
log.F("path", r.URL.Path),
|
|
||||||
log.F("error", err))
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -42,9 +41,7 @@ func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
|||||||
// Validate token
|
// Validate token
|
||||||
claims, err := jwtManager.ValidateToken(tokenString)
|
claims, err := jwtManager.ValidateToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Authentication failed - invalid token",
|
logger.Warn("Authentication failed - invalid token")
|
||||||
log.F("path", r.URL.Path),
|
|
||||||
log.F("error", err))
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -60,21 +57,17 @@ func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
|||||||
func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
|
func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger := log.FromContext(r.Context())
|
||||||
claims, ok := r.Context().Value(ClaimsContextKey).(*Claims)
|
claims, ok := r.Context().Value(ClaimsContextKey).(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.LogWarn("Authorization failed - no claims in context",
|
logger.Warn("Authorization failed - no claims in context")
|
||||||
log.F("path", r.URL.Path),
|
|
||||||
log.F("required_role", requiredRole))
|
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtManager := NewJWTManager()
|
jwtManager := NewJWTManager()
|
||||||
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
||||||
log.LogWarn("Authorization failed - insufficient role",
|
logger.With("user_role", claims.Role).With("required_role", requiredRole).Warn("Authorization failed - insufficient role")
|
||||||
log.F("path", r.URL.Path),
|
|
||||||
log.F("user_role", claims.Role),
|
|
||||||
log.F("required_role", requiredRole))
|
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -88,6 +81,7 @@ func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
|
|||||||
func GraphQLAuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
func GraphQLAuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger := log.FromContext(r.Context())
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@ -96,20 +90,22 @@ func GraphQLAuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handl
|
|||||||
|
|
||||||
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("GraphQL authentication failed - could not extract token", log.F("error", err))
|
logger.Error(err, "GraphQL authentication failed - could not extract token")
|
||||||
next.ServeHTTP(w, r) // Proceed without auth
|
next.ServeHTTP(w, r) // Proceed without auth
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := jwtManager.ValidateToken(tokenString)
|
claims, err := jwtManager.ValidateToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("GraphQL authentication failed - invalid token", log.F("error", err))
|
logger.Error(err, "GraphQL authentication failed - invalid token")
|
||||||
next.ServeHTTP(w, r) // Proceed without auth
|
next.ServeHTTP(w, r) // Proceed without auth
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add claims to context for authenticated requests
|
// Add claims and enriched logger to context for authenticated requests
|
||||||
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
||||||
|
enrichedLogger := logger.With("user_id", claims.UserID)
|
||||||
|
ctx = context.WithValue(ctx, observability.LoggerContextKey, enrichedLogger)
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
4
internal/platform/cache/redis_cache.go
vendored
4
internal/platform/cache/redis_cache.go
vendored
@ -112,9 +112,7 @@ func (c *RedisCache) GetMulti(ctx context.Context, keys []string) (map[string][]
|
|||||||
|
|
||||||
str, ok := values[i].(string)
|
str, ok := values[i].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.LogWarn("Invalid type in Redis cache",
|
log.FromContext(ctx).With("key", key).With("type", fmt.Sprintf("%T", values[i])).Warn("Invalid type in Redis cache")
|
||||||
log.F("key", key),
|
|
||||||
log.F("type", fmt.Sprintf("%T", values[i])))
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"tercul/internal/observability"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
@ -16,10 +17,8 @@ var DB *gorm.DB
|
|||||||
|
|
||||||
// Connect establishes a connection to the database using configuration settings
|
// Connect establishes a connection to the database using configuration settings
|
||||||
// It returns the database connection and any error encountered
|
// It returns the database connection and any error encountered
|
||||||
func Connect() (*gorm.DB, error) {
|
func Connect(metrics *observability.Metrics) (*gorm.DB, error) {
|
||||||
log.LogInfo("Connecting to database",
|
log.Info(fmt.Sprintf("Connecting to database: host=%s db=%s", config.Cfg.DBHost, config.Cfg.DBName))
|
||||||
log.F("host", config.Cfg.DBHost),
|
|
||||||
log.F("database", config.Cfg.DBName))
|
|
||||||
|
|
||||||
dsn := config.Cfg.GetDSN()
|
dsn := config.Cfg.GetDSN()
|
||||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
@ -29,6 +28,11 @@ func Connect() (*gorm.DB, error) {
|
|||||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register Prometheus plugin
|
||||||
|
if err := db.Use(NewPrometheusPlugin(metrics)); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to register prometheus plugin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Set the global DB instance
|
// Set the global DB instance
|
||||||
DB = db
|
DB = db
|
||||||
|
|
||||||
@ -43,9 +47,7 @@ func Connect() (*gorm.DB, error) {
|
|||||||
sqlDB.SetMaxIdleConns(5) // Idle connections
|
sqlDB.SetMaxIdleConns(5) // Idle connections
|
||||||
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||||
|
|
||||||
log.LogInfo("Successfully connected to database",
|
log.Info(fmt.Sprintf("Successfully connected to database: host=%s db=%s", config.Cfg.DBHost, config.Cfg.DBName))
|
||||||
log.F("host", config.Cfg.DBHost),
|
|
||||||
log.F("database", config.Cfg.DBName))
|
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
@ -66,9 +68,9 @@ func Close() error {
|
|||||||
|
|
||||||
// InitDB initializes the database connection and runs migrations
|
// InitDB initializes the database connection and runs migrations
|
||||||
// It returns the database connection and any error encountered
|
// It returns the database connection and any error encountered
|
||||||
func InitDB() (*gorm.DB, error) {
|
func InitDB(metrics *observability.Metrics) (*gorm.DB, error) {
|
||||||
// Connect to the database
|
// Connect to the database
|
||||||
db, err := Connect()
|
db, err := Connect(metrics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
76
internal/platform/db/prometheus.go
Normal file
76
internal/platform/db/prometheus.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tercul/internal/observability"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
callBackBeforeName = "prometheus:before"
|
||||||
|
callBackAfterName = "prometheus:after"
|
||||||
|
startTime = "start_time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PrometheusPlugin struct {
|
||||||
|
Metrics *observability.Metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrometheusPlugin) Name() string {
|
||||||
|
return "PrometheusPlugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrometheusPlugin) Initialize(db *gorm.DB) error {
|
||||||
|
// Before callbacks
|
||||||
|
db.Callback().Create().Before("gorm:create").Register(callBackBeforeName, p.before)
|
||||||
|
db.Callback().Query().Before("gorm:query").Register(callBackBeforeName, p.before)
|
||||||
|
db.Callback().Update().Before("gorm:update").Register(callBackBeforeName, p.before)
|
||||||
|
db.Callback().Delete().Before("gorm:delete").Register(callBackBeforeName, p.before)
|
||||||
|
db.Callback().Row().Before("gorm:row").Register(callBackBeforeName, p.before)
|
||||||
|
db.Callback().Raw().Before("gorm:raw").Register(callBackBeforeName, p.before)
|
||||||
|
|
||||||
|
// After callbacks
|
||||||
|
db.Callback().Create().After("gorm:create").Register(callBackAfterName, p.after)
|
||||||
|
db.Callback().Query().After("gorm:query").Register(callBackAfterName, p.after)
|
||||||
|
db.Callback().Update().After("gorm:update").Register(callBackAfterName, p.after)
|
||||||
|
db.Callback().Delete().After("gorm:delete").Register(callBackAfterName, p.after)
|
||||||
|
db.Callback().Row().After("gorm:row").Register(callBackAfterName, p.after)
|
||||||
|
db.Callback().Raw().After("gorm:raw").Register(callBackAfterName, p.after)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrometheusPlugin) before(db *gorm.DB) {
|
||||||
|
db.Set(startTime, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrometheusPlugin) after(db *gorm.DB) {
|
||||||
|
_ts, ok := db.Get(startTime)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, ok := _ts.(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
operation := db.Statement.SQL.String()
|
||||||
|
if len(operation) > 50 { // Truncate long queries
|
||||||
|
operation = operation[:50]
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "success"
|
||||||
|
if db.Error != nil {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(ts).Seconds()
|
||||||
|
p.Metrics.DBQueryDuration.WithLabelValues(operation, status).Observe(duration)
|
||||||
|
p.Metrics.DBQueriesTotal.WithLabelValues(operation, status).Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPrometheusPlugin(metrics *observability.Metrics) *PrometheusPlugin {
|
||||||
|
return &PrometheusPlugin{Metrics: metrics}
|
||||||
|
}
|
||||||
@ -85,9 +85,9 @@ func RateLimitMiddleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// Check if request is allowed
|
// Check if request is allowed
|
||||||
if !rateLimiter.Allow(clientID) {
|
if !rateLimiter.Allow(clientID) {
|
||||||
log.LogWarn("Rate limit exceeded",
|
log.FromContext(r.Context()).
|
||||||
log.F("clientID", clientID),
|
With("clientID", clientID).
|
||||||
log.F("path", r.URL.Path))
|
Warn("Rate limit exceeded")
|
||||||
|
|
||||||
w.WriteHeader(http.StatusTooManyRequests)
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
w.Write([]byte("Rate limit exceeded. Please try again later."))
|
w.Write([]byte("Rate limit exceeded. Please try again later."))
|
||||||
|
|||||||
@ -8,232 +8,96 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LogLevel represents the severity level of a log message.
|
// Logger is a wrapper around the observability logger.
|
||||||
type LogLevel int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DebugLevel for detailed troubleshooting.
|
|
||||||
DebugLevel LogLevel = iota
|
|
||||||
// InfoLevel for general operational information.
|
|
||||||
InfoLevel
|
|
||||||
// WarnLevel for potentially harmful situations.
|
|
||||||
WarnLevel
|
|
||||||
// ErrorLevel for error events that might still allow the application to continue.
|
|
||||||
ErrorLevel
|
|
||||||
// FatalLevel for severe error events that will lead the application to abort.
|
|
||||||
FatalLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
// Field represents a key-value pair for structured logging.
|
|
||||||
type Field struct {
|
|
||||||
Key string
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// F creates a new Field.
|
|
||||||
func F(key string, value interface{}) Field {
|
|
||||||
return Field{Key: key, Value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger provides structured logging capabilities.
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
*observability.Logger
|
*observability.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultLogger = &Logger{observability.NewLogger("tercul", "development")}
|
// defaultLogger is the global fallback logger.
|
||||||
|
var defaultLogger = observability.NewLogger("tercul", "development")
|
||||||
|
|
||||||
// Init re-initializes the default logger. This is useful for applications
|
// Init re-initializes the default logger. This is useful for applications
|
||||||
// that need to configure the logger with dynamic values.
|
// that need to configure the logger with dynamic values from config.
|
||||||
func Init(serviceName, environment string) {
|
func Init(serviceName, environment string) {
|
||||||
defaultLogger = &Logger{observability.NewLogger(serviceName, environment)}
|
defaultLogger = observability.NewLogger(serviceName, environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultLevel sets the log level for the default logger.
|
// FromContext retrieves the request-scoped logger from the context.
|
||||||
func SetDefaultLevel(level LogLevel) {
|
// If no logger is found, it returns the default global logger.
|
||||||
var zlevel zerolog.Level
|
func FromContext(ctx context.Context) *Logger {
|
||||||
switch level {
|
// We wrap the observability.Logger in our platform.Logger
|
||||||
case DebugLevel:
|
return &Logger{observability.LoggerFromContext(ctx)}
|
||||||
zlevel = zerolog.DebugLevel
|
|
||||||
case InfoLevel:
|
|
||||||
zlevel = zerolog.InfoLevel
|
|
||||||
case WarnLevel:
|
|
||||||
zlevel = zerolog.WarnLevel
|
|
||||||
case ErrorLevel:
|
|
||||||
zlevel = zerolog.ErrorLevel
|
|
||||||
case FatalLevel:
|
|
||||||
zlevel = zerolog.FatalLevel
|
|
||||||
default:
|
|
||||||
zlevel = zerolog.InfoLevel
|
|
||||||
}
|
|
||||||
zerolog.SetGlobalLevel(zlevel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func log(level LogLevel, msg string, fields ...Field) {
|
// SetLevel sets the global log level.
|
||||||
var event *zerolog.Event
|
func SetLevel(level zerolog.Level) {
|
||||||
// Access the embedded observability.Logger to get to zerolog's methods.
|
zerolog.SetGlobalLevel(level)
|
||||||
zlog := defaultLogger.Logger
|
|
||||||
switch level {
|
|
||||||
case DebugLevel:
|
|
||||||
event = zlog.Debug()
|
|
||||||
case InfoLevel:
|
|
||||||
event = zlog.Info()
|
|
||||||
case WarnLevel:
|
|
||||||
event = zlog.Warn()
|
|
||||||
case ErrorLevel:
|
|
||||||
event = zlog.Error()
|
|
||||||
case FatalLevel:
|
|
||||||
event = zlog.Fatal()
|
|
||||||
default:
|
|
||||||
event = zlog.Info()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range fields {
|
|
||||||
event.Interface(f.Key, f.Value)
|
|
||||||
}
|
|
||||||
event.Msg(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogDebug logs a message at debug level using the default logger.
|
|
||||||
func LogDebug(msg string, fields ...Field) {
|
|
||||||
log(DebugLevel, msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogInfo logs a message at info level using the default logger.
|
|
||||||
func LogInfo(msg string, fields ...Field) {
|
|
||||||
log(InfoLevel, msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogWarn logs a message at warn level using the default logger.
|
|
||||||
func LogWarn(msg string, fields ...Field) {
|
|
||||||
log(WarnLevel, msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogError logs a message at error level using the default logger.
|
|
||||||
func LogError(msg string, fields ...Field) {
|
|
||||||
log(ErrorLevel, msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogFatal logs a message at fatal level using the default logger and then calls os.Exit(1).
|
|
||||||
func LogFatal(msg string, fields ...Field) {
|
|
||||||
log(FatalLevel, msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFields returns a new logger with the given fields added using the default logger.
|
|
||||||
func WithFields(fields ...Field) *Logger {
|
|
||||||
sublogger := defaultLogger.With().Logger()
|
|
||||||
for _, f := range fields {
|
|
||||||
sublogger = sublogger.With().Interface(f.Key, f.Value).Logger()
|
|
||||||
}
|
|
||||||
return &Logger{&observability.Logger{&sublogger}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithContext returns a new logger with the given context added using the default logger.
|
|
||||||
func WithContext(ctx context.Context) *Logger {
|
|
||||||
return &Logger{defaultLogger.Ctx(ctx)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following functions are kept for compatibility but are now simplified or deprecated.
|
|
||||||
|
|
||||||
// SetDefaultLogger is deprecated. Use Init.
|
|
||||||
func SetDefaultLogger(logger *Logger) {
|
|
||||||
// Deprecated: Logger is now initialized via Init.
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of the log level.
|
|
||||||
func (l LogLevel) String() string {
|
|
||||||
switch l {
|
|
||||||
case DebugLevel:
|
|
||||||
return "DEBUG"
|
|
||||||
case InfoLevel:
|
|
||||||
return "INFO"
|
|
||||||
case WarnLevel:
|
|
||||||
return "WARN"
|
|
||||||
case ErrorLevel:
|
|
||||||
return "ERROR"
|
|
||||||
case FatalLevel:
|
|
||||||
return "FATAL"
|
|
||||||
default:
|
|
||||||
return "UNKNOWN"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a message at debug level.
|
// Debug logs a message at debug level.
|
||||||
func (l *Logger) Debug(msg string, fields ...Field) {
|
func (l *Logger) Debug(msg string) {
|
||||||
l.log(DebugLevel, msg, fields...)
|
l.Logger.Debug().Msg(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a message at info level.
|
// Info logs a message at info level.
|
||||||
func (l *Logger) Info(msg string, fields ...Field) {
|
func (l *Logger) Info(msg string) {
|
||||||
l.log(InfoLevel, msg, fields...)
|
l.Logger.Info().Msg(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a message at warn level.
|
// Warn logs a message at warn level.
|
||||||
func (l *Logger) Warn(msg string, fields ...Field) {
|
func (l *Logger) Warn(msg string) {
|
||||||
l.log(WarnLevel, msg, fields...)
|
l.Logger.Warn().Msg(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs a message at error level.
|
// Error logs a message at error level.
|
||||||
func (l *Logger) Error(msg string, fields ...Field) {
|
func (l *Logger) Error(err error, msg string) {
|
||||||
l.log(ErrorLevel, msg, fields...)
|
l.Logger.Error().Err(err).Msg(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a message at fatal level and then calls os.Exit(1).
|
// Fatal logs a message at fatal level and then calls os.Exit(1).
|
||||||
func (l *Logger) Fatal(msg string, fields ...Field) {
|
func (l *Logger) Fatal(err error, msg string) {
|
||||||
l.log(FatalLevel, msg, fields...)
|
l.Logger.Fatal().Err(err).Msg(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) log(level LogLevel, msg string, fields ...Field) {
|
// With adds a key-value pair to the logger's context.
|
||||||
var event *zerolog.Event
|
func (l *Logger) With(key string, value interface{}) *Logger {
|
||||||
switch level {
|
return &Logger{l.Logger.With(key, value)}
|
||||||
case DebugLevel:
|
|
||||||
event = l.Logger.Debug()
|
|
||||||
case InfoLevel:
|
|
||||||
event = l.Logger.Info()
|
|
||||||
case WarnLevel:
|
|
||||||
event = l.Logger.Warn()
|
|
||||||
case ErrorLevel:
|
|
||||||
event = l.Logger.Error()
|
|
||||||
case FatalLevel:
|
|
||||||
event = l.Logger.Fatal()
|
|
||||||
default:
|
|
||||||
event = l.Logger.Info()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range fields {
|
|
||||||
event.Interface(f.Key, f.Value)
|
|
||||||
}
|
|
||||||
event.Msg(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFields returns a new logger with the given fields added.
|
// Infof logs a formatted message at info level.
|
||||||
func (l *Logger) WithFields(fields ...Field) *Logger {
|
func (l *Logger) Infof(format string, v ...interface{}) {
|
||||||
sublogger := l.With().Logger()
|
l.Info(fmt.Sprintf(format, v...))
|
||||||
for _, f := range fields {
|
|
||||||
sublogger = sublogger.With().Interface(f.Key, f.Value).Logger()
|
|
||||||
}
|
|
||||||
return &Logger{&observability.Logger{&sublogger}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) WithContext(ctx map[string]interface{}) *Logger {
|
// Errorf logs a formatted message at error level.
|
||||||
// To maintain compatibility with the old API, we will convert the map to a context.
|
func (l *Logger) Errorf(err error, format string, v ...interface{}) {
|
||||||
// This is not ideal and should be refactored in the future.
|
l.Error(err, fmt.Sprintf(format, v...))
|
||||||
zlog := l.Logger.With().Logger()
|
|
||||||
for k, v := range ctx {
|
|
||||||
zlog = zlog.With().Interface(k, v).Logger()
|
|
||||||
}
|
|
||||||
return &Logger{&observability.Logger{&zlog}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) SetLevel(level LogLevel) {
|
// The following functions use the default logger and are kept for convenience
|
||||||
// This now controls the global log level.
|
// in areas where a context is not available.
|
||||||
SetDefaultLevel(level)
|
|
||||||
|
// Debug logs a message at debug level using the default logger.
|
||||||
|
func Debug(msg string) {
|
||||||
|
defaultLogger.Debug().Msg(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fmt versions for simple string formatting
|
// Info logs a message at info level using the default logger.
|
||||||
func LogInfof(format string, v ...interface{}) {
|
func Info(msg string) {
|
||||||
log(InfoLevel, fmt.Sprintf(format, v...))
|
defaultLogger.Info().Msg(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogErrorf(format string, v ...interface{}) {
|
// Warn logs a message at warn level using the default logger.
|
||||||
log(ErrorLevel, fmt.Sprintf(format, v...))
|
func Warn(msg string) {
|
||||||
|
defaultLogger.Warn().Msg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs a message at error level using the default logger.
|
||||||
|
func Error(err error, msg string) {
|
||||||
|
defaultLogger.Error().Err(err).Msg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a message at fatal level using the default logger.
|
||||||
|
func Fatal(err error, msg string) {
|
||||||
|
defaultLogger.Fatal().Err(err).Msg(msg)
|
||||||
}
|
}
|
||||||
60
internal/platform/openlibrary/client.go
Normal file
60
internal/platform/openlibrary/client.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package openlibrary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseURL = "https://openlibrary.org"
|
||||||
|
|
||||||
|
// Client is a client for the Open Library API.
|
||||||
|
type Client struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Open Library client.
|
||||||
|
func NewClient() *Client {
|
||||||
|
return &Client{
|
||||||
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author represents the data returned from the Open Library Authors API.
|
||||||
|
type Author struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
PersonalName string `json:"personal_name"`
|
||||||
|
Bio interface{} `json:"bio"` // Bio can be a string or a struct
|
||||||
|
Wikipedia string `json:"wikipedia"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthor fetches author data from the Open Library API.
|
||||||
|
// The olid is the Open Library Author ID (e.g., "OL23919A").
|
||||||
|
func (c *Client) GetAuthor(ctx context.Context, olid string) (*Author, error) {
|
||||||
|
url := fmt.Sprintf("%s/authors/%s.json", baseURL, olid)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "TerculEnrichmentTool/1.0 (contact@tercul.com)")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var author Author
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&author); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &author, nil
|
||||||
|
}
|
||||||
@ -35,7 +35,7 @@ func (m *MockTranslationRepository) GetByID(ctx context.Context, id uint) (*doma
|
|||||||
return &cp, nil
|
return &cp, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, ErrEntityNotFound
|
return nil, domain.ErrEntityNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTranslationRepository) Update(ctx context.Context, t *domain.Translation) error {
|
func (m *MockTranslationRepository) Update(ctx context.Context, t *domain.Translation) error {
|
||||||
@ -45,7 +45,7 @@ func (m *MockTranslationRepository) Update(ctx context.Context, t *domain.Transl
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ErrEntityNotFound
|
return domain.ErrEntityNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTranslationRepository) Delete(ctx context.Context, id uint) error {
|
func (m *MockTranslationRepository) Delete(ctx context.Context, id uint) error {
|
||||||
@ -55,7 +55,7 @@ func (m *MockTranslationRepository) Delete(ctx context.Context, id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ErrEntityNotFound
|
return domain.ErrEntityNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
func (m *MockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||||
|
|||||||
@ -1,93 +1,41 @@
|
|||||||
package testutil
|
package testutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
"tercul/internal/platform/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrEntityNotFound = errors.New("entity not found")
|
// BaseSuite is a base test suite with common functionality for tests.
|
||||||
|
// It is designed for unit and mock-based integration tests and does not
|
||||||
// TestDB holds the test database connection
|
// handle database connections.
|
||||||
var TestDB *gorm.DB
|
type BaseSuite struct {
|
||||||
|
suite.Suite
|
||||||
// SetupTestDB sets up a test database connection
|
|
||||||
func SetupTestDB() (*gorm.DB, error) {
|
|
||||||
// Load configuration
|
|
||||||
config.LoadConfig()
|
|
||||||
|
|
||||||
// Use test-specific environment variables if available, otherwise fall back to main config
|
|
||||||
host := getEnv("TEST_DB_HOST", config.Cfg.DBHost)
|
|
||||||
port := getEnv("TEST_DB_PORT", config.Cfg.DBPort)
|
|
||||||
user := getEnv("TEST_DB_USER", config.Cfg.DBUser)
|
|
||||||
password := getEnv("TEST_DB_PASSWORD", config.Cfg.DBPassword)
|
|
||||||
dbname := getEnv("TEST_DB_NAME", "tercul_test") // Always use test database
|
|
||||||
sslmode := getEnv("TEST_DB_SSLMODE", config.Cfg.DBSSLMode)
|
|
||||||
|
|
||||||
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
|
||||||
host, port, user, password, dbname, sslmode)
|
|
||||||
|
|
||||||
// Custom logger for tests
|
|
||||||
newLogger := logger.New(
|
|
||||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
|
||||||
logger.Config{
|
|
||||||
SlowThreshold: time.Second,
|
|
||||||
LogLevel: logger.Silent, // Silent during tests
|
|
||||||
IgnoreRecordNotFoundError: true,
|
|
||||||
Colorful: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
||||||
Logger: newLogger,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to test database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set connection pool settings
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get SQL DB instance: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB.SetMaxOpenConns(5)
|
|
||||||
sqlDB.SetMaxIdleConns(2)
|
|
||||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TruncateTables truncates all tables in the test database
|
// SetupSuite can be overridden by specific test suites for setup.
|
||||||
func TruncateTables(db *gorm.DB, tables ...string) error {
|
func (s *BaseSuite) SetupSuite() {
|
||||||
for _, table := range tables {
|
// No-op by default.
|
||||||
if err := db.Exec(fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table)).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseDB closes the test database connection
|
// TearDownSuite can be overridden by specific test suites for teardown.
|
||||||
func CloseDB(db *gorm.DB) error {
|
func (s *BaseSuite) TearDownSuite() {
|
||||||
sqlDB, err := db.DB()
|
// No-op by default.
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sqlDB.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnv gets an environment variable or returns a default value
|
// SetupTest can be overridden by specific test suites for per-test setup.
|
||||||
|
func (s *BaseSuite) SetupTest() {
|
||||||
|
// No-op by default.
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownTest can be overridden by specific test suites for per-test teardown.
|
||||||
|
func (s *BaseSuite) TearDownTest() {
|
||||||
|
// No-op by default.
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnv gets an environment variable or returns a default value.
|
||||||
|
// This is kept as a general utility function.
|
||||||
func getEnv(key, defaultValue string) string {
|
func getEnv(key, defaultValue string) string {
|
||||||
value, exists := os.LookupEnv(key)
|
value, exists := os.LookupEnv(key)
|
||||||
if !exists {
|
if !exists {
|
||||||
@ -96,61 +44,7 @@ func getEnv(key, defaultValue string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseSuite is a base test suite with common functionality
|
// SkipIfShort skips a test if the -short flag is provided.
|
||||||
// For integration tests using mocks, DB is not used
|
|
||||||
// TODO: Remove DB logic for mock-based integration tests (priority: high, effort: medium)
|
|
||||||
type BaseSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
// DB *gorm.DB // Removed for mock-based integration tests
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupSuite sets up the test suite
|
|
||||||
func (s *BaseSuite) SetupSuite() {
|
|
||||||
// No DB setup for mock-based integration tests
|
|
||||||
}
|
|
||||||
|
|
||||||
// TearDownSuite tears down the test suite
|
|
||||||
func (s *BaseSuite) TearDownSuite() {
|
|
||||||
// No DB teardown for mock-based integration tests
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupTest sets up each test
|
|
||||||
func (s *BaseSuite) SetupTest() {
|
|
||||||
// Can be overridden by specific test suites
|
|
||||||
}
|
|
||||||
|
|
||||||
// TearDownTest tears down each test
|
|
||||||
func (s *BaseSuite) TearDownTest() {
|
|
||||||
// Can be overridden by specific test suites
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunTransactional runs a test function in a transaction
|
|
||||||
// TODO: Remove or refactor for mock-based tests (priority: low, effort: low)
|
|
||||||
func (s *BaseSuite) RunTransactional(testFunc func(tx interface{})) {
|
|
||||||
// No-op for mock-based tests
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockDB creates a mock database for testing
|
|
||||||
func MockDB() (*sql.DB, error) {
|
|
||||||
// Use environment variables for test database connection
|
|
||||||
host := getEnv("TEST_DB_HOST", "localhost")
|
|
||||||
port := getEnv("TEST_DB_PORT", "5432")
|
|
||||||
user := getEnv("TEST_DB_USER", "postgres")
|
|
||||||
password := getEnv("TEST_DB_PASSWORD", "postgres")
|
|
||||||
dbname := getEnv("TEST_DB_NAME", "tercul_test")
|
|
||||||
sslmode := getEnv("TEST_DB_SSLMODE", "disable")
|
|
||||||
|
|
||||||
dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s",
|
|
||||||
user, password, host, port, dbname, sslmode)
|
|
||||||
|
|
||||||
db, err := sql.Open("postgres", dsn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SkipIfShort skips a test if the -short flag is provided
|
|
||||||
func SkipIfShort(t *testing.T) {
|
func SkipIfShort(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("Skipping test in short mode")
|
t.Skip("Skipping test in short mode")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user