feat: Implement critical features and fix build

This commit addresses several high-priority tasks from the TASKS.md file, including:

- **Fix Background Job Panic:** Replaced `log.Fatalf` with `log.Printf` in the `asynq` server to prevent crashes.
- **Refactor API Server Setup:** Consolidated the GraphQL Playground and Prometheus metrics endpoints into the main API server.
- **Implement `DeleteUser` Mutation:** Implemented the `DeleteUser` resolver.
- **Implement `CreateContribution` Mutation:** Implemented the `CreateContribution` resolver and its required application service.

Additionally, this commit includes a major refactoring of the configuration management system to fix a broken build. The global `config.Cfg` variable has been removed and replaced with a dependency injection approach, where the configuration object is passed to all components that require it. This change has been applied across the entire codebase, including the test suite, to ensure a stable and testable application.
This commit is contained in:
google-labs-jules[bot] 2025-10-05 18:29:18 +00:00
parent 37a007b08c
commit a8dfb727a1
67 changed files with 641 additions and 365 deletions

View File

@ -3,11 +3,10 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"runtime"
"syscall" "syscall"
"tercul/internal/app" "tercul/internal/app"
"tercul/internal/app/analytics" "tercul/internal/app/analytics"
@ -18,7 +17,7 @@ import (
"tercul/internal/platform/auth" "tercul/internal/platform/auth"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"tercul/internal/platform/db" "tercul/internal/platform/db"
"tercul/internal/platform/log" app_log "tercul/internal/platform/log"
"tercul/internal/platform/search" "tercul/internal/platform/search"
"time" "time"
@ -30,7 +29,7 @@ import (
) )
// runMigrations applies database migrations using goose. // runMigrations applies database migrations using goose.
func runMigrations(gormDB *gorm.DB) error { func runMigrations(gormDB *gorm.DB, migrationPath string) error {
sqlDB, err := gormDB.DB() sqlDB, err := gormDB.DB()
if err != nil { if err != nil {
return err return err
@ -40,35 +39,34 @@ func runMigrations(gormDB *gorm.DB) error {
return err return err
} }
// This is brittle. A better approach might be to use an env var or config. app_log.Info(fmt.Sprintf("Applying database migrations from %s", migrationPath))
_, b, _, _ := runtime.Caller(0) if err := goose.Up(sqlDB, migrationPath); err != nil {
migrationsDir := filepath.Join(filepath.Dir(b), "../../internal/data/migrations")
log.Info(fmt.Sprintf("Applying database migrations from %s", migrationsDir))
if err := goose.Up(sqlDB, migrationsDir); err != nil {
return err return err
} }
log.Info("Database migrations applied successfully") app_log.Info("Database migrations applied successfully")
return nil return nil
} }
// main is the entry point for the Tercul application. // main is the entry point for the Tercul application.
func main() { func main() {
// Load configuration from environment variables // Load configuration from environment variables
config.LoadConfig() cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("cannot load config: %v", err)
}
// Initialize logger // Initialize logger
log.Init("tercul-api", config.Cfg.Environment) app_log.Init("tercul-api", cfg.Environment)
obsLogger := observability.NewLogger("tercul-api", config.Cfg.Environment) obsLogger := observability.NewLogger("tercul-api", 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", cfg.Environment)
if err != nil { if err != nil {
log.Fatal(err, "Failed to initialize OpenTelemetry tracer") app_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.Error(err, "Error shutting down tracer provider") app_log.Error(err, "Error shutting down tracer provider")
} }
}() }()
@ -76,44 +74,44 @@ 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.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", config.Cfg.Environment)) app_log.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", cfg.Environment))
// Initialize database connection // Initialize database connection
database, err := db.InitDB(metrics) database, err := db.InitDB(cfg, metrics)
if err != nil { if err != nil {
log.Fatal(err, "Failed to initialize database") app_log.Fatal(err, "Failed to initialize database")
} }
defer db.Close() defer db.Close(database)
if err := runMigrations(database); err != nil { if err := runMigrations(database, cfg.MigrationPath); err != nil {
log.Fatal(err, "Failed to apply database migrations") app_log.Fatal(err, "Failed to apply database migrations")
} }
// Initialize Weaviate client // Initialize Weaviate client
weaviateCfg := weaviate.Config{ weaviateCfg := weaviate.Config{
Host: config.Cfg.WeaviateHost, Host: cfg.WeaviateHost,
Scheme: config.Cfg.WeaviateScheme, Scheme: cfg.WeaviateScheme,
} }
weaviateClient, err := weaviate.NewClient(weaviateCfg) weaviateClient, err := weaviate.NewClient(weaviateCfg)
if err != nil { if err != nil {
log.Fatal(err, "Failed to create weaviate client") app_log.Fatal(err, "Failed to create weaviate client")
} }
// Create search client // Create search client
searchClient := search.NewWeaviateWrapper(weaviateClient) searchClient := search.NewWeaviateWrapper(weaviateClient)
// Create repositories // Create repositories
repos := dbsql.NewRepositories(database) repos := dbsql.NewRepositories(database, cfg)
// Create linguistics dependencies // Create linguistics dependencies
analysisRepo := linguistics.NewGORMAnalysisRepository(database) analysisRepo := linguistics.NewGORMAnalysisRepository(database)
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider() sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
if err != nil { if err != nil {
log.Fatal(err, "Failed to create sentiment provider") app_log.Fatal(err, "Failed to create sentiment provider")
} }
// Create platform components // Create platform components
jwtManager := auth.NewJWTManager() jwtManager := auth.NewJWTManager(cfg)
// Create application services // Create application services
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
@ -135,6 +133,7 @@ func main() {
SourceRepo: repos.Source, SourceRepo: repos.Source,
CopyrightRepo: repos.Copyright, CopyrightRepo: repos.Copyright,
MonetizationRepo: repos.Monetization, MonetizationRepo: repos.Monetization,
ContributionRepo: repos.Contribution,
AnalyticsRepo: repos.Analytics, AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth, AuthRepo: repos.Auth,
LocalizationRepo: repos.Localization, LocalizationRepo: repos.Localization,
@ -151,28 +150,27 @@ func main() {
App: application, App: application,
} }
// Create handlers // Create the main API handler with all middleware.
// NewServerWithAuth now returns the handler chain directly.
apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger) apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
playgroundHandler := playground.Handler("GraphQL Playground", "/query")
metricsHandler := observability.PrometheusHandler(reg)
// Consolidate handlers into a single mux // Create the main ServeMux and register all handlers.
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/query", apiHandler) mux.Handle("/query", apiHandler)
mux.Handle("/playground", playgroundHandler) mux.Handle("/playground", playground.Handler("GraphQL Playground", "/query"))
mux.Handle("/metrics", metricsHandler) mux.Handle("/metrics", observability.PrometheusHandler(reg))
// Create a single HTTP server // Create a single HTTP server with the main mux.
mainServer := &http.Server{ mainServer := &http.Server{
Addr: config.Cfg.ServerPort, Addr: cfg.ServerPort,
Handler: mux, Handler: mux,
} }
log.Info(fmt.Sprintf("API server listening on port %s", config.Cfg.ServerPort)) app_log.Info(fmt.Sprintf("API server listening on port %s", cfg.ServerPort))
// Start the main server in a goroutine // Start the main server in a goroutine
go func() { go func() {
if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start server") app_log.Fatal(err, "Failed to start server")
} }
}() }()
@ -180,15 +178,15 @@ func main() {
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Info("Shutting down server...") app_log.Info("Shutting down server...")
// 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 := mainServer.Shutdown(ctx); err != nil { if err := mainServer.Shutdown(ctx); err != nil {
log.Error(err, "Server forced to shutdown") app_log.Error(err, "Server forced to shutdown")
} }
log.Info("Server shut down successfully") app_log.Info("Server shut down successfully")
} }

View File

@ -9,19 +9,6 @@ import (
"github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler"
) )
// NewServer creates a new GraphQL server with the given resolver
func NewServer(resolver *graphql.Resolver) http.Handler {
c := graphql.Config{Resolvers: resolver}
c.Directives.Binding = graphql.Binding
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
mux := http.NewServeMux()
mux.Handle("/query", srv)
return mux
}
// 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, logger *observability.Logger) 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}
@ -42,9 +29,6 @@ func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager,
chain = observability.TracingMiddleware(chain) chain = observability.TracingMiddleware(chain)
chain = observability.RequestIDMiddleware(chain) chain = observability.RequestIDMiddleware(chain)
// Create a mux to handle GraphQL endpoint // Return the handler chain directly. The caller is responsible for routing.
mux := http.NewServeMux() return chain
mux.Handle("/query", chain)
return mux
} }

View File

@ -31,15 +31,18 @@ func main() {
} }
// 2. Initialize dependencies // 2. Initialize dependencies
config.LoadConfig() cfg, err := config.LoadConfig()
if err != nil {
log.Fatal(err, "Failed to load config")
}
log.Init("enrich-tool", "development") log.Init("enrich-tool", "development")
database, err := db.InitDB(nil) // No metrics needed for this tool database, err := db.InitDB(cfg, nil) // No metrics needed for this tool
if err != nil { if err != nil {
log.Fatal(err, "Failed to initialize database") log.Fatal(err, "Failed to initialize database")
} }
defer db.Close() defer db.Close(database)
repos := sql.NewRepositories(database) repos := sql.NewRepositories(database, cfg)
enrichmentSvc := enrichment.NewService() enrichmentSvc := enrichment.NewService()
// 3. Fetch, enrich, and save the entity // 3. Fetch, enrich, and save the entity

View File

@ -20,6 +20,7 @@ import (
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/observability" "tercul/internal/observability"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
platform_config "tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler"
@ -58,7 +59,9 @@ func (s *GraphQLIntegrationSuite) CreateAuthenticatedUser(username, email string
user.Role = role user.Role = role
// Re-generate token with the new role // Re-generate token with the new role
jwtManager := platform_auth.NewJWTManager() cfg, err := platform_config.LoadConfig()
s.Require().NoError(err)
jwtManager := platform_auth.NewJWTManager(cfg)
newToken, err := jwtManager.GenerateToken(user) newToken, err := jwtManager.GenerateToken(user)
s.Require().NoError(err) s.Require().NoError(err)
token = newToken token = newToken
@ -81,7 +84,9 @@ func (s *GraphQLIntegrationSuite) SetupSuite() {
srv.SetErrorPresenter(graph.NewErrorPresenter()) srv.SetErrorPresenter(graph.NewErrorPresenter())
// Create JWT manager and middleware // Create JWT manager and middleware
jwtManager := platform_auth.NewJWTManager() cfg, err := platform_config.LoadConfig()
s.Require().NoError(err)
jwtManager := platform_auth.NewJWTManager(cfg)
reg := prometheus.NewRegistry() reg := prometheus.NewRegistry()
metrics := observability.NewMetrics(reg) metrics := observability.NewMetrics(reg)

View File

@ -15,6 +15,7 @@ import (
"tercul/internal/app/bookmark" "tercul/internal/app/bookmark"
"tercul/internal/app/collection" "tercul/internal/app/collection"
"tercul/internal/app/comment" "tercul/internal/app/comment"
"tercul/internal/app/contribution"
"tercul/internal/app/like" "tercul/internal/app/like"
"tercul/internal/app/translation" "tercul/internal/app/translation"
"tercul/internal/app/user" "tercul/internal/app/user"
@ -502,7 +503,17 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
// DeleteUser is the resolver for the deleteUser field. // DeleteUser is the resolver for the deleteUser field.
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
panic(fmt.Errorf("not implemented: DeleteUser - deleteUser")) userID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
}
err = r.App.User.Commands.DeleteUser(ctx, uint(userID))
if err != nil {
return false, err
}
return true, nil
} }
// CreateCollection is the resolver for the createCollection field. // CreateCollection is the resolver for the createCollection field.
@ -1020,7 +1031,56 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
// CreateContribution is the resolver for the createContribution field. // CreateContribution is the resolver for the createContribution field.
func (r *mutationResolver) CreateContribution(ctx context.Context, input model.ContributionInput) (*model.Contribution, error) { func (r *mutationResolver) CreateContribution(ctx context.Context, input model.ContributionInput) (*model.Contribution, error) {
panic(fmt.Errorf("not implemented: CreateContribution - createContribution")) // Get user ID from context
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthorized")
}
// Convert GraphQL input to service input
createInput := contribution.CreateContributionInput{
Name: input.Name,
}
if input.WorkID != nil {
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
wID := uint(workID)
createInput.WorkID = &wID
}
if input.TranslationID != nil {
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
tID := uint(translationID)
createInput.TranslationID = &tID
}
if input.Status != nil {
createInput.Status = input.Status.String()
} else {
createInput.Status = "DRAFT" // Default status
}
// Call contribution service
createdContribution, err := r.App.Contribution.Commands.CreateContribution(ctx, createInput)
if err != nil {
return nil, err
}
// Convert to GraphQL model
return &model.Contribution{
ID: fmt.Sprintf("%d", createdContribution.ID),
Name: createdContribution.Name,
Status: model.ContributionStatus(createdContribution.Status),
User: &model.User{
ID: fmt.Sprintf("%d", userID),
},
}, nil
} }
// UpdateContribution is the resolver for the updateContribution field. // UpdateContribution is the resolver for the updateContribution field.

View File

@ -9,6 +9,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/jobs/linguistics" "tercul/internal/jobs/linguistics"
"tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -21,10 +22,12 @@ type AnalyticsServiceTestSuite struct {
func (s *AnalyticsServiceTestSuite) SetupSuite() { func (s *AnalyticsServiceTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
analyticsRepo := sql.NewAnalyticsRepository(s.DB) cfg, err := config.LoadConfig()
s.Require().NoError(err)
analyticsRepo := sql.NewAnalyticsRepository(s.DB, cfg)
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB) analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
translationRepo := sql.NewTranslationRepository(s.DB) translationRepo := sql.NewTranslationRepository(s.DB, cfg)
workRepo := sql.NewWorkRepository(s.DB) workRepo := sql.NewWorkRepository(s.DB, cfg)
sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider() sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider()
s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, workRepo, sentimentProvider) s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, workRepo, sentimentProvider)
} }

View File

@ -2,18 +2,20 @@ package app
import ( import (
"tercul/internal/app/analytics" "tercul/internal/app/analytics"
"tercul/internal/app/auth"
"tercul/internal/app/author" "tercul/internal/app/author"
"tercul/internal/app/authz"
"tercul/internal/app/book" "tercul/internal/app/book"
"tercul/internal/app/bookmark" "tercul/internal/app/bookmark"
"tercul/internal/app/category" "tercul/internal/app/category"
"tercul/internal/app/collection" "tercul/internal/app/collection"
"tercul/internal/app/comment" "tercul/internal/app/comment"
"tercul/internal/app/contribution"
"tercul/internal/app/like" "tercul/internal/app/like"
"tercul/internal/app/localization" "tercul/internal/app/localization"
"tercul/internal/app/tag" "tercul/internal/app/tag"
"tercul/internal/app/translation" "tercul/internal/app/translation"
"tercul/internal/app/user" "tercul/internal/app/user"
"tercul/internal/app/auth"
"tercul/internal/app/work" "tercul/internal/app/work"
"tercul/internal/domain" "tercul/internal/domain"
auth_domain "tercul/internal/domain/auth" auth_domain "tercul/internal/domain/auth"
@ -23,8 +25,6 @@ import (
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
) )
import "tercul/internal/app/authz"
// Dependencies holds all external dependencies for the application. // Dependencies holds all external dependencies for the application.
type Dependencies struct { type Dependencies struct {
WorkRepo work_domain.WorkRepository WorkRepo work_domain.WorkRepository
@ -42,6 +42,7 @@ type Dependencies struct {
SourceRepo domain.SourceRepository SourceRepo domain.SourceRepository
CopyrightRepo domain.CopyrightRepository CopyrightRepo domain.CopyrightRepository
MonetizationRepo domain.MonetizationRepository MonetizationRepo domain.MonetizationRepository
ContributionRepo domain.ContributionRepository
AnalyticsRepo analytics.Repository AnalyticsRepo analytics.Repository
AuthRepo auth_domain.AuthRepository AuthRepo auth_domain.AuthRepository
LocalizationRepo localization_domain.LocalizationRepository LocalizationRepo localization_domain.LocalizationRepository
@ -58,6 +59,7 @@ type Application struct {
Category *category.Service Category *category.Service
Collection *collection.Service Collection *collection.Service
Comment *comment.Service Comment *comment.Service
Contribution *contribution.Service
Like *like.Service Like *like.Service
Tag *tag.Service Tag *tag.Service
Translation *translation.Service Translation *translation.Service
@ -77,6 +79,8 @@ func NewApplication(deps Dependencies) *Application {
categoryService := category.NewService(deps.CategoryRepo) categoryService := category.NewService(deps.CategoryRepo)
collectionService := collection.NewService(deps.CollectionRepo) collectionService := collection.NewService(deps.CollectionRepo)
commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService) commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService)
contributionCommands := contribution.NewCommands(deps.ContributionRepo, authzService)
contributionService := contribution.NewService(contributionCommands)
likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService) likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService)
tagService := tag.NewService(deps.TagRepo) tagService := tag.NewService(deps.TagRepo)
translationService := translation.NewService(deps.TranslationRepo, authzService) translationService := translation.NewService(deps.TranslationRepo, authzService)
@ -92,6 +96,7 @@ func NewApplication(deps Dependencies) *Application {
Category: categoryService, Category: categoryService,
Collection: collectionService, Collection: collectionService,
Comment: commentService, Comment: commentService,
Contribution: contributionService,
Like: likeService, Like: likeService,
Tag: tagService, Tag: tagService,
Translation: translationService, Translation: translationService,

View File

@ -0,0 +1,55 @@
package contribution
import (
"context"
"tercul/internal/app/authz"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
)
// Commands contains the command handlers for the contribution aggregate.
type Commands struct {
repo domain.ContributionRepository
authzSvc *authz.Service
}
// NewCommands creates a new Commands handler.
func NewCommands(repo domain.ContributionRepository, authzSvc *authz.Service) *Commands {
return &Commands{
repo: repo,
authzSvc: authzSvc,
}
}
// CreateContributionInput represents the input for creating a new contribution.
type CreateContributionInput struct {
Name string
Status string
WorkID *uint
TranslationID *uint
}
// CreateContribution creates a new contribution.
func (c *Commands) CreateContribution(ctx context.Context, input CreateContributionInput) (*domain.Contribution, error) {
actorID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, domain.ErrUnauthorized
}
// TODO: Add authorization check using authzSvc if necessary
contribution := &domain.Contribution{
Name: input.Name,
Status: input.Status,
UserID: actorID,
WorkID: input.WorkID,
TranslationID: input.TranslationID,
}
err := c.repo.Create(ctx, contribution)
if err != nil {
return nil, err
}
return contribution, nil
}

View File

@ -0,0 +1,14 @@
package contribution
// Service encapsulates the contribution-related business logic.
type Service struct {
Commands *Commands
// Queries *Queries // Queries can be added here later if needed
}
// NewService creates a new contribution service.
func NewService(commands *Commands) *Service {
return &Service{
Commands: commands,
}
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"testing" "testing"
"tercul/internal/platform/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -172,7 +173,9 @@ func TestMergeWork_Integration(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Create real repositories and services pointing to the test DB // Create real repositories and services pointing to the test DB
workRepo := sql.NewWorkRepository(db) cfg, err := config.LoadConfig()
assert.NoError(t, err)
workRepo := sql.NewWorkRepository(db, cfg)
authzSvc := authz.NewService(workRepo, nil) // Using real repo for authz checks authzSvc := authz.NewService(workRepo, nil) // Using real repo for authz checks
searchClient := &mockSearchClient{} // Mock search client is fine searchClient := &mockSearchClient{} // Mock search client is fine
commands := NewWorkCommands(workRepo, searchClient, authzSvc) commands := NewWorkCommands(workRepo, searchClient, authzSvc)

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/app/analytics" "tercul/internal/app/analytics"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/platform/config"
"time" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
@ -18,7 +19,7 @@ type analyticsRepository struct {
tracer trace.Tracer tracer trace.Tracer
} }
func NewAnalyticsRepository(db *gorm.DB) analytics.Repository { func NewAnalyticsRepository(db *gorm.DB, cfg *config.Config) analytics.Repository {
return &analyticsRepository{ return &analyticsRepository{
db: db, db: db,
tracer: otel.Tracer("analytics.repository"), tracer: otel.Tracer("analytics.repository"),

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain/auth" "tercul/internal/domain/auth"
"tercul/internal/platform/config"
"time" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
@ -15,7 +16,7 @@ type authRepository struct {
tracer trace.Tracer tracer trace.Tracer
} }
func NewAuthRepository(db *gorm.DB) auth.AuthRepository { func NewAuthRepository(db *gorm.DB, cfg *config.Config) auth.AuthRepository {
return &authRepository{ return &authRepository{
db: db, db: db,
tracer: otel.Tracer("auth.repository"), tracer: otel.Tracer("auth.repository"),

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type authorRepository struct {
} }
// NewAuthorRepository creates a new AuthorRepository. // NewAuthorRepository creates a new AuthorRepository.
func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository { func NewAuthorRepository(db *gorm.DB, cfg *config.Config) domain.AuthorRepository {
return &authorRepository{ return &authorRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Author](db), BaseRepository: NewBaseRepositoryImpl[domain.Author](db, cfg),
db: db, db: db,
tracer: otel.Tracer("author.repository"), tracer: otel.Tracer("author.repository"),
} }

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -17,7 +18,9 @@ type AuthorRepositoryTestSuite struct {
func (s *AuthorRepositoryTestSuite) SetupSuite() { func (s *AuthorRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.AuthorRepo = sql.NewAuthorRepository(s.DB) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.AuthorRepo = sql.NewAuthorRepository(s.DB, cfg)
} }
func (s *AuthorRepositoryTestSuite) SetupTest() { func (s *AuthorRepositoryTestSuite) SetupTest() {

View File

@ -28,13 +28,15 @@ var (
type BaseRepositoryImpl[T any] struct { type BaseRepositoryImpl[T any] struct {
db *gorm.DB db *gorm.DB
tracer trace.Tracer tracer trace.Tracer
cfg *config.Config
} }
// 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, cfg *config.Config) domain.BaseRepository[T] {
return &BaseRepositoryImpl[T]{ return &BaseRepositoryImpl[T]{
db: db, db: db,
tracer: otel.Tracer("base.repository"), tracer: otel.Tracer("base.repository"),
cfg: cfg,
} }
} }
@ -69,7 +71,7 @@ func (r *BaseRepositoryImpl[T]) validatePagination(page, pageSize int) (int, int
} }
if pageSize < 1 { if pageSize < 1 {
pageSize = config.Cfg.PageSize pageSize = r.cfg.PageSize
if pageSize < 1 { if pageSize < 1 {
pageSize = 20 // Default page size pageSize = 20 // Default page size
} }
@ -525,7 +527,7 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
defer span.End() defer span.End()
if batchSize <= 0 { if batchSize <= 0 {
batchSize = config.Cfg.BatchSize batchSize = r.cfg.BatchSize
if batchSize <= 0 { if batchSize <= 0 {
batchSize = 100 // Default batch size batchSize = 100 // Default batch size
} }

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -16,12 +17,16 @@ import (
type BaseRepositoryTestSuite struct { type BaseRepositoryTestSuite struct {
testutil.IntegrationTestSuite testutil.IntegrationTestSuite
repo domain.BaseRepository[testutil.TestEntity] repo domain.BaseRepository[testutil.TestEntity]
cfg *config.Config
} }
// SetupSuite initializes the test suite, database, and repository. // SetupSuite initializes the test suite, database, and repository.
func (s *BaseRepositoryTestSuite) SetupSuite() { func (s *BaseRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.cfg = cfg
s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB, s.cfg)
} }
// SetupTest cleans the database before each test. // SetupTest cleans the database before each test.
@ -219,7 +224,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
// Act // Act
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error { err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
entity := &testutil.TestEntity{Name: "TX Commit"} entity := &testutil.TestEntity{Name: "TX Commit"}
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx) repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
if err := repoInTx.Create(context.Background(), entity); err != nil { if err := repoInTx.Create(context.Background(), entity); err != nil {
return err return err
} }
@ -241,7 +246,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
// Act // Act
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error { err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
entity := &testutil.TestEntity{Name: "TX Rollback"} entity := &testutil.TestEntity{Name: "TX Rollback"}
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx) repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
if err := repoInTx.Create(context.Background(), entity); err != nil { if err := repoInTx.Create(context.Background(), entity); err != nil {
return err return err
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type bookRepository struct {
} }
// NewBookRepository creates a new BookRepository. // NewBookRepository creates a new BookRepository.
func NewBookRepository(db *gorm.DB) domain.BookRepository { func NewBookRepository(db *gorm.DB, cfg *config.Config) domain.BookRepository {
return &bookRepository{ return &bookRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Book](db), BaseRepository: NewBaseRepositoryImpl[domain.Book](db, cfg),
db: db, db: db,
tracer: otel.Tracer("book.repository"), tracer: otel.Tracer("book.repository"),
} }

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -17,7 +18,9 @@ type BookRepositoryTestSuite struct {
func (s *BookRepositoryTestSuite) SetupSuite() { func (s *BookRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.BookRepo = sql.NewBookRepository(s.DB) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.BookRepo = sql.NewBookRepository(s.DB, cfg)
} }
func (s *BookRepositoryTestSuite) SetupTest() { func (s *BookRepositoryTestSuite) SetupTest() {

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type bookmarkRepository struct {
} }
// NewBookmarkRepository creates a new BookmarkRepository. // NewBookmarkRepository creates a new BookmarkRepository.
func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository { func NewBookmarkRepository(db *gorm.DB, cfg *config.Config) domain.BookmarkRepository {
return &bookmarkRepository{ return &bookmarkRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db), BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db, cfg),
db: db, db: db,
tracer: otel.Tracer("bookmark.repository"), tracer: otel.Tracer("bookmark.repository"),
} }

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
repo "tercul/internal/data/sql" repo "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewBookmarkRepository(t *testing.T) { func TestNewBookmarkRepository(t *testing.T) {
db, _, err := newMockDb() db, _, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewBookmarkRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
assert.NotNil(t, repo) assert.NotNil(t, repo)
} }
@ -25,7 +28,9 @@ func TestBookmarkRepository_ListByUserID(t *testing.T) {
t.Run("should return bookmarks for a given user id", func(t *testing.T) { t.Run("should return bookmarks for a given user id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewBookmarkRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
userID := uint(1) userID := uint(1)
expectedBookmarks := []domain.Bookmark{ expectedBookmarks := []domain.Bookmark{
@ -50,7 +55,9 @@ func TestBookmarkRepository_ListByUserID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewBookmarkRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
userID := uint(1) userID := uint(1)
@ -69,7 +76,9 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) {
t.Run("should return bookmarks for a given work id", func(t *testing.T) { t.Run("should return bookmarks for a given work id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewBookmarkRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
workID := uint(1) workID := uint(1)
expectedBookmarks := []domain.Bookmark{ expectedBookmarks := []domain.Bookmark{
@ -94,7 +103,9 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewBookmarkRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewBookmarkRepository(db, cfg)
workID := uint(1) workID := uint(1)

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type categoryRepository struct {
} }
// NewCategoryRepository creates a new CategoryRepository. // NewCategoryRepository creates a new CategoryRepository.
func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository { func NewCategoryRepository(db *gorm.DB, cfg *config.Config) domain.CategoryRepository {
return &categoryRepository{ return &categoryRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Category](db), BaseRepository: NewBaseRepositoryImpl[domain.Category](db, cfg),
db: db, db: db,
tracer: otel.Tracer("category.repository"), tracer: otel.Tracer("category.repository"),
} }

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -17,7 +18,9 @@ type CategoryRepositoryTestSuite struct {
func (s *CategoryRepositoryTestSuite) SetupSuite() { func (s *CategoryRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.CategoryRepo = sql.NewCategoryRepository(s.DB) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.CategoryRepo = sql.NewCategoryRepository(s.DB, cfg)
} }
func (s *CategoryRepositoryTestSuite) SetupTest() { func (s *CategoryRepositoryTestSuite) SetupTest() {

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -13,9 +14,9 @@ type cityRepository struct {
} }
// NewCityRepository creates a new CityRepository. // NewCityRepository creates a new CityRepository.
func NewCityRepository(db *gorm.DB) domain.CityRepository { func NewCityRepository(db *gorm.DB, cfg *config.Config) domain.CityRepository {
return &cityRepository{ return &cityRepository{
BaseRepository: NewBaseRepositoryImpl[domain.City](db), BaseRepository: NewBaseRepositoryImpl[domain.City](db, cfg),
db: db, db: db,
} }
} }

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
repo "tercul/internal/data/sql" repo "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewCityRepository(t *testing.T) { func TestNewCityRepository(t *testing.T) {
db, _, err := newMockDb() db, _, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCityRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCityRepository(db, cfg)
assert.NotNil(t, repo) assert.NotNil(t, repo)
} }
@ -25,7 +28,9 @@ func TestCityRepository_ListByCountryID(t *testing.T) {
t.Run("should return cities for a given country id", func(t *testing.T) { t.Run("should return cities for a given country id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCityRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCityRepository(db, cfg)
countryID := uint(1) countryID := uint(1)
expectedCities := []domain.City{ expectedCities := []domain.City{
@ -50,7 +55,9 @@ func TestCityRepository_ListByCountryID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCityRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCityRepository(db, cfg)
countryID := uint(1) countryID := uint(1)

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type collectionRepository struct {
} }
// NewCollectionRepository creates a new CollectionRepository. // NewCollectionRepository creates a new CollectionRepository.
func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository { func NewCollectionRepository(db *gorm.DB, cfg *config.Config) domain.CollectionRepository {
return &collectionRepository{ return &collectionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db), BaseRepository: NewBaseRepositoryImpl[domain.Collection](db, cfg),
db: db, db: db,
tracer: otel.Tracer("collection.repository"), tracer: otel.Tracer("collection.repository"),
} }

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -28,7 +29,9 @@ func (s *CollectionRepositoryTestSuite) SetupTest() {
s.db = gormDB s.db = gormDB
s.mock = mock s.mock = mock
s.repo = sql.NewCollectionRepository(s.db) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.repo = sql.NewCollectionRepository(s.db, cfg)
} }
func (s *CollectionRepositoryTestSuite) TearDownTest() { func (s *CollectionRepositoryTestSuite) TearDownTest() {

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type commentRepository struct {
} }
// NewCommentRepository creates a new CommentRepository. // NewCommentRepository creates a new CommentRepository.
func NewCommentRepository(db *gorm.DB) domain.CommentRepository { func NewCommentRepository(db *gorm.DB, cfg *config.Config) domain.CommentRepository {
return &commentRepository{ return &commentRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db), BaseRepository: NewBaseRepositoryImpl[domain.Comment](db, cfg),
db: db, db: db,
tracer: otel.Tracer("comment.repository"), tracer: otel.Tracer("comment.repository"),
} }

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
repo "tercul/internal/data/sql" repo "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewCommentRepository(t *testing.T) { func TestNewCommentRepository(t *testing.T) {
db, _, err := newMockDb() db, _, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
assert.NotNil(t, repo) assert.NotNil(t, repo)
} }
@ -25,7 +28,9 @@ func TestCommentRepository_ListByUserID(t *testing.T) {
t.Run("should return comments for a given user id", func(t *testing.T) { t.Run("should return comments for a given user id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
userID := uint(1) userID := uint(1)
expectedComments := []domain.Comment{ expectedComments := []domain.Comment{
@ -50,7 +55,9 @@ func TestCommentRepository_ListByUserID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
userID := uint(1) userID := uint(1)
@ -69,7 +76,9 @@ func TestCommentRepository_ListByWorkID(t *testing.T) {
t.Run("should return comments for a given work id", func(t *testing.T) { t.Run("should return comments for a given work id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
workID := uint(1) workID := uint(1)
expectedComments := []domain.Comment{ expectedComments := []domain.Comment{
@ -94,7 +103,9 @@ func TestCommentRepository_ListByWorkID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
workID := uint(1) workID := uint(1)
@ -113,7 +124,9 @@ func TestCommentRepository_ListByTranslationID(t *testing.T) {
t.Run("should return comments for a given translation id", func(t *testing.T) { t.Run("should return comments for a given translation id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
translationID := uint(1) translationID := uint(1)
expectedComments := []domain.Comment{ expectedComments := []domain.Comment{
@ -138,7 +151,9 @@ func TestCommentRepository_ListByTranslationID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
translationID := uint(1) translationID := uint(1)
@ -157,7 +172,9 @@ func TestCommentRepository_ListByParentID(t *testing.T) {
t.Run("should return comments for a given parent id", func(t *testing.T) { t.Run("should return comments for a given parent id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
parentID := uint(1) parentID := uint(1)
expectedComments := []domain.Comment{ expectedComments := []domain.Comment{
@ -182,7 +199,9 @@ func TestCommentRepository_ListByParentID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewCommentRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewCommentRepository(db, cfg)
parentID := uint(1) parentID := uint(1)

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type contributionRepository struct {
} }
// NewContributionRepository creates a new ContributionRepository. // NewContributionRepository creates a new ContributionRepository.
func NewContributionRepository(db *gorm.DB) domain.ContributionRepository { func NewContributionRepository(db *gorm.DB, cfg *config.Config) domain.ContributionRepository {
return &contributionRepository{ return &contributionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db), BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db, cfg),
db: db, db: db,
tracer: otel.Tracer("contribution.repository"), tracer: otel.Tracer("contribution.repository"),
} }

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
repo "tercul/internal/data/sql" repo "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
@ -17,7 +18,9 @@ import (
func TestNewContributionRepository(t *testing.T) { func TestNewContributionRepository(t *testing.T) {
db, _, err := newMockDb() db, _, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
assert.NotNil(t, repo) assert.NotNil(t, repo)
} }
@ -25,7 +28,9 @@ func TestContributionRepository_ListByUserID(t *testing.T) {
t.Run("should return contributions for a given user id", func(t *testing.T) { t.Run("should return contributions for a given user id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
userID := uint(1) userID := uint(1)
expectedContributions := []domain.Contribution{ expectedContributions := []domain.Contribution{
@ -50,7 +55,9 @@ func TestContributionRepository_ListByUserID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
userID := uint(1) userID := uint(1)
@ -69,7 +76,9 @@ func TestContributionRepository_ListByReviewerID(t *testing.T) {
t.Run("should return contributions for a given reviewer id", func(t *testing.T) { t.Run("should return contributions for a given reviewer id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
reviewerID := uint(1) reviewerID := uint(1)
expectedContributions := []domain.Contribution{ expectedContributions := []domain.Contribution{
@ -94,7 +103,9 @@ func TestContributionRepository_ListByReviewerID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
reviewerID := uint(1) reviewerID := uint(1)
@ -113,7 +124,9 @@ func TestContributionRepository_ListByWorkID(t *testing.T) {
t.Run("should return contributions for a given work id", func(t *testing.T) { t.Run("should return contributions for a given work id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
workID := uint(1) workID := uint(1)
expectedContributions := []domain.Contribution{ expectedContributions := []domain.Contribution{
@ -138,7 +151,9 @@ func TestContributionRepository_ListByWorkID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
workID := uint(1) workID := uint(1)
@ -157,7 +172,9 @@ func TestContributionRepository_ListByTranslationID(t *testing.T) {
t.Run("should return contributions for a given translation id", func(t *testing.T) { t.Run("should return contributions for a given translation id", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
translationID := uint(1) translationID := uint(1)
expectedContributions := []domain.Contribution{ expectedContributions := []domain.Contribution{
@ -182,7 +199,9 @@ func TestContributionRepository_ListByTranslationID(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
translationID := uint(1) translationID := uint(1)
@ -201,7 +220,9 @@ func TestContributionRepository_ListByStatus(t *testing.T) {
t.Run("should return contributions for a given status", func(t *testing.T) { t.Run("should return contributions for a given status", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
status := "draft" status := "draft"
expectedContributions := []domain.Contribution{ expectedContributions := []domain.Contribution{
@ -226,7 +247,9 @@ func TestContributionRepository_ListByStatus(t *testing.T) {
t.Run("should return error if query fails", func(t *testing.T) { t.Run("should return error if query fails", func(t *testing.T) {
db, mock, err := newMockDb() db, mock, err := newMockDb()
require.NoError(t, err) require.NoError(t, err)
repo := repo.NewContributionRepository(db) cfg, err := config.LoadConfig()
require.NoError(t, err)
repo := repo.NewContributionRepository(db, cfg)
status := "draft" status := "draft"

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type copyrightClaimRepository struct {
} }
// NewCopyrightClaimRepository creates a new CopyrightClaimRepository. // NewCopyrightClaimRepository creates a new CopyrightClaimRepository.
func NewCopyrightClaimRepository(db *gorm.DB) domain.CopyrightClaimRepository { func NewCopyrightClaimRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightClaimRepository {
return &copyrightClaimRepository{ return &copyrightClaimRepository{
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db), BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db, cfg),
db: db, db: db,
tracer: otel.Tracer("copyright_claim.repository"), tracer: otel.Tracer("copyright_claim.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type copyrightRepository struct {
} }
// NewCopyrightRepository creates a new CopyrightRepository. // NewCopyrightRepository creates a new CopyrightRepository.
func NewCopyrightRepository(db *gorm.DB) domain.CopyrightRepository { func NewCopyrightRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightRepository {
return &copyrightRepository{ return &copyrightRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db), BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db, cfg),
db: db, db: db,
tracer: otel.Tracer("copyright.repository"), tracer: otel.Tracer("copyright.repository"),
} }

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
@ -41,7 +42,9 @@ func (s *CopyrightRepositoryTestSuite) SetupTest() {
s.db = gormDB s.db = gormDB
s.mock = mock s.mock = mock
s.repo = sql.NewCopyrightRepository(s.db) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.repo = sql.NewCopyrightRepository(s.db, cfg)
} }
// TearDownTest checks if all expectations were met. // TearDownTest checks if all expectations were met.

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -14,9 +15,9 @@ type countryRepository struct {
} }
// NewCountryRepository creates a new CountryRepository. // NewCountryRepository creates a new CountryRepository.
func NewCountryRepository(db *gorm.DB) domain.CountryRepository { func NewCountryRepository(db *gorm.DB, cfg *config.Config) domain.CountryRepository {
return &countryRepository{ return &countryRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Country](db), BaseRepository: NewBaseRepositoryImpl[domain.Country](db, cfg),
db: db, db: db,
} }
} }

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type edgeRepository struct {
} }
// NewEdgeRepository creates a new EdgeRepository. // NewEdgeRepository creates a new EdgeRepository.
func NewEdgeRepository(db *gorm.DB) domain.EdgeRepository { func NewEdgeRepository(db *gorm.DB, cfg *config.Config) domain.EdgeRepository {
return &edgeRepository{ return &edgeRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Edge](db), BaseRepository: NewBaseRepositoryImpl[domain.Edge](db, cfg),
db: db, db: db,
tracer: otel.Tracer("edge.repository"), tracer: otel.Tracer("edge.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type editionRepository struct {
} }
// NewEditionRepository creates a new EditionRepository. // NewEditionRepository creates a new EditionRepository.
func NewEditionRepository(db *gorm.DB) domain.EditionRepository { func NewEditionRepository(db *gorm.DB, cfg *config.Config) domain.EditionRepository {
return &editionRepository{ return &editionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db), BaseRepository: NewBaseRepositoryImpl[domain.Edition](db, cfg),
db: db, db: db,
tracer: otel.Tracer("edition.repository"), tracer: otel.Tracer("edition.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
@ -18,9 +19,9 @@ type emailVerificationRepository struct {
} }
// NewEmailVerificationRepository creates a new EmailVerificationRepository. // NewEmailVerificationRepository creates a new EmailVerificationRepository.
func NewEmailVerificationRepository(db *gorm.DB) domain.EmailVerificationRepository { func NewEmailVerificationRepository(db *gorm.DB, cfg *config.Config) domain.EmailVerificationRepository {
return &emailVerificationRepository{ return &emailVerificationRepository{
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db), BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db, cfg),
db: db, db: db,
tracer: otel.Tracer("email_verification.repository"), tracer: otel.Tracer("email_verification.repository"),
} }

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type likeRepository struct {
} }
// NewLikeRepository creates a new LikeRepository. // NewLikeRepository creates a new LikeRepository.
func NewLikeRepository(db *gorm.DB) domain.LikeRepository { func NewLikeRepository(db *gorm.DB, cfg *config.Config) domain.LikeRepository {
return &likeRepository{ return &likeRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Like](db), BaseRepository: NewBaseRepositoryImpl[domain.Like](db, cfg),
db: db, db: db,
tracer: otel.Tracer("like.repository"), tracer: otel.Tracer("like.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/localization" "tercul/internal/domain/localization"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -15,7 +16,7 @@ type localizationRepository struct {
tracer trace.Tracer tracer trace.Tracer
} }
func NewLocalizationRepository(db *gorm.DB) localization.LocalizationRepository { func NewLocalizationRepository(db *gorm.DB, cfg *config.Config) localization.LocalizationRepository {
return &localizationRepository{ return &localizationRepository{
db: db, db: db,
tracer: otel.Tracer("localization.repository"), tracer: otel.Tracer("localization.repository"),

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type monetizationRepository struct {
} }
// NewMonetizationRepository creates a new MonetizationRepository. // NewMonetizationRepository creates a new MonetizationRepository.
func NewMonetizationRepository(db *gorm.DB) domain.MonetizationRepository { func NewMonetizationRepository(db *gorm.DB, cfg *config.Config) domain.MonetizationRepository {
return &monetizationRepository{ return &monetizationRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db), BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db, cfg),
db: db, db: db,
tracer: otel.Tracer("monetization.repository"), tracer: otel.Tracer("monetization.repository"),
} }

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
workdomain "tercul/internal/domain/work" workdomain "tercul/internal/domain/work"
"tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -18,7 +19,9 @@ type MonetizationRepositoryTestSuite struct {
func (s *MonetizationRepositoryTestSuite) SetupSuite() { func (s *MonetizationRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.MonetizationRepo = sql.NewMonetizationRepository(s.DB) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.MonetizationRepo = sql.NewMonetizationRepository(s.DB, cfg)
} }
func (s *MonetizationRepositoryTestSuite) SetupTest() { func (s *MonetizationRepositoryTestSuite) SetupTest() {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
@ -18,9 +19,9 @@ type passwordResetRepository struct {
} }
// NewPasswordResetRepository creates a new PasswordResetRepository. // NewPasswordResetRepository creates a new PasswordResetRepository.
func NewPasswordResetRepository(db *gorm.DB) domain.PasswordResetRepository { func NewPasswordResetRepository(db *gorm.DB, cfg *config.Config) domain.PasswordResetRepository {
return &passwordResetRepository{ return &passwordResetRepository{
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db), BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db, cfg),
db: db, db: db,
tracer: otel.Tracer("password_reset.repository"), tracer: otel.Tracer("password_reset.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"math" "math"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type placeRepository struct {
} }
// NewPlaceRepository creates a new PlaceRepository. // NewPlaceRepository creates a new PlaceRepository.
func NewPlaceRepository(db *gorm.DB) domain.PlaceRepository { func NewPlaceRepository(db *gorm.DB, cfg *config.Config) domain.PlaceRepository {
return &placeRepository{ return &placeRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Place](db), BaseRepository: NewBaseRepositoryImpl[domain.Place](db, cfg),
db: db, db: db,
tracer: otel.Tracer("place.repository"), tracer: otel.Tracer("place.repository"),
} }

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -16,9 +17,9 @@ type publisherRepository struct {
} }
// NewPublisherRepository creates a new PublisherRepository. // NewPublisherRepository creates a new PublisherRepository.
func NewPublisherRepository(db *gorm.DB) domain.PublisherRepository { func NewPublisherRepository(db *gorm.DB, cfg *config.Config) domain.PublisherRepository {
return &publisherRepository{ return &publisherRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db), BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db, cfg),
db: db, db: db,
tracer: otel.Tracer("publisher.repository"), tracer: otel.Tracer("publisher.repository"),
} }

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/domain/auth" "tercul/internal/domain/auth"
"tercul/internal/domain/localization" "tercul/internal/domain/localization"
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/platform/config"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -26,31 +27,33 @@ type Repositories struct {
Source domain.SourceRepository Source domain.SourceRepository
Copyright domain.CopyrightRepository Copyright domain.CopyrightRepository
Monetization domain.MonetizationRepository Monetization domain.MonetizationRepository
Contribution domain.ContributionRepository
Analytics analytics.Repository Analytics analytics.Repository
Auth auth.AuthRepository Auth auth.AuthRepository
Localization localization.LocalizationRepository Localization localization.LocalizationRepository
} }
// NewRepositories creates a new Repositories container // NewRepositories creates a new Repositories container
func NewRepositories(db *gorm.DB) *Repositories { func NewRepositories(db *gorm.DB, cfg *config.Config) *Repositories {
return &Repositories{ return &Repositories{
Work: NewWorkRepository(db), Work: NewWorkRepository(db, cfg),
User: NewUserRepository(db), User: NewUserRepository(db, cfg),
Author: NewAuthorRepository(db), Author: NewAuthorRepository(db, cfg),
Translation: NewTranslationRepository(db), Translation: NewTranslationRepository(db, cfg),
Comment: NewCommentRepository(db), Comment: NewCommentRepository(db, cfg),
Like: NewLikeRepository(db), Like: NewLikeRepository(db, cfg),
Bookmark: NewBookmarkRepository(db), Bookmark: NewBookmarkRepository(db, cfg),
Collection: NewCollectionRepository(db), Collection: NewCollectionRepository(db, cfg),
Tag: NewTagRepository(db), Tag: NewTagRepository(db, cfg),
Category: NewCategoryRepository(db), Category: NewCategoryRepository(db, cfg),
Book: NewBookRepository(db), Book: NewBookRepository(db, cfg),
Publisher: NewPublisherRepository(db), Publisher: NewPublisherRepository(db, cfg),
Source: NewSourceRepository(db), Source: NewSourceRepository(db, cfg),
Copyright: NewCopyrightRepository(db), Copyright: NewCopyrightRepository(db, cfg),
Monetization: NewMonetizationRepository(db), Monetization: NewMonetizationRepository(db, cfg),
Analytics: NewAnalyticsRepository(db), Contribution: NewContributionRepository(db, cfg),
Auth: NewAuthRepository(db), Analytics: NewAnalyticsRepository(db, cfg),
Localization: NewLocalizationRepository(db), Auth: NewAuthRepository(db, cfg),
Localization: NewLocalizationRepository(db, cfg),
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type sourceRepository struct {
} }
// NewSourceRepository creates a new SourceRepository. // NewSourceRepository creates a new SourceRepository.
func NewSourceRepository(db *gorm.DB) domain.SourceRepository { func NewSourceRepository(db *gorm.DB, cfg *config.Config) domain.SourceRepository {
return &sourceRepository{ return &sourceRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Source](db), BaseRepository: NewBaseRepositoryImpl[domain.Source](db, cfg),
db: db, db: db,
tracer: otel.Tracer("source.repository"), tracer: otel.Tracer("source.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type tagRepository struct {
} }
// NewTagRepository creates a new TagRepository. // NewTagRepository creates a new TagRepository.
func NewTagRepository(db *gorm.DB) domain.TagRepository { func NewTagRepository(db *gorm.DB, cfg *config.Config) domain.TagRepository {
return &tagRepository{ return &tagRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db), BaseRepository: NewBaseRepositoryImpl[domain.Tag](db, cfg),
db: db, db: db,
tracer: otel.Tracer("tag.repository"), tracer: otel.Tracer("tag.repository"),
} }

View File

@ -3,6 +3,7 @@ package sql
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type translationRepository struct {
} }
// NewTranslationRepository creates a new TranslationRepository. // NewTranslationRepository creates a new TranslationRepository.
func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository { func NewTranslationRepository(db *gorm.DB, cfg *config.Config) domain.TranslationRepository {
return &translationRepository{ return &translationRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db), BaseRepository: NewBaseRepositoryImpl[domain.Translation](db, cfg),
db: db, db: db,
tracer: otel.Tracer("translation.repository"), tracer: otel.Tracer("translation.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type userProfileRepository struct {
} }
// NewUserProfileRepository creates a new UserProfileRepository. // NewUserProfileRepository creates a new UserProfileRepository.
func NewUserProfileRepository(db *gorm.DB) domain.UserProfileRepository { func NewUserProfileRepository(db *gorm.DB, cfg *config.Config) domain.UserProfileRepository {
return &userProfileRepository{ return &userProfileRepository{
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db), BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db, cfg),
db: db, db: db,
tracer: otel.Tracer("user_profile.repository"), tracer: otel.Tracer("user_profile.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -17,9 +18,9 @@ type userRepository struct {
} }
// NewUserRepository creates a new UserRepository. // NewUserRepository creates a new UserRepository.
func NewUserRepository(db *gorm.DB) domain.UserRepository { func NewUserRepository(db *gorm.DB, cfg *config.Config) domain.UserRepository {
return &userRepository{ return &userRepository{
BaseRepository: NewBaseRepositoryImpl[domain.User](db), BaseRepository: NewBaseRepositoryImpl[domain.User](db, cfg),
db: db, db: db,
tracer: otel.Tracer("user.repository"), tracer: otel.Tracer("user.repository"),
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config"
"time" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
@ -18,9 +19,9 @@ type userSessionRepository struct {
} }
// NewUserSessionRepository creates a new UserSessionRepository. // NewUserSessionRepository creates a new UserSessionRepository.
func NewUserSessionRepository(db *gorm.DB) domain.UserSessionRepository { func NewUserSessionRepository(db *gorm.DB, cfg *config.Config) domain.UserSessionRepository {
return &userSessionRepository{ return &userSessionRepository{
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db), BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db, cfg),
db: db, db: db,
tracer: otel.Tracer("user_session.repository"), tracer: otel.Tracer("user_session.repository"),
} }

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/platform/config"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -19,9 +20,9 @@ type workRepository struct {
} }
// NewWorkRepository creates a new WorkRepository. // NewWorkRepository creates a new WorkRepository.
func NewWorkRepository(db *gorm.DB) work.WorkRepository { func NewWorkRepository(db *gorm.DB, cfg *config.Config) work.WorkRepository {
return &workRepository{ return &workRepository{
BaseRepository: NewBaseRepositoryImpl[work.Work](db), BaseRepository: NewBaseRepositoryImpl[work.Work](db, cfg),
db: db, db: db,
tracer: otel.Tracer("work.repository"), tracer: otel.Tracer("work.repository"),
} }

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -18,7 +19,9 @@ type WorkRepositoryTestSuite struct {
func (s *WorkRepositoryTestSuite) SetupSuite() { func (s *WorkRepositoryTestSuite) SetupSuite() {
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
s.WorkRepo = sql.NewWorkRepository(s.DB) cfg, err := config.LoadConfig()
s.Require().NoError(err)
s.WorkRepo = sql.NewWorkRepository(s.DB, cfg)
} }
func (s *WorkRepositoryTestSuite) TestCreateWork() { func (s *WorkRepositoryTestSuite) TestCreateWork() {

View File

@ -31,9 +31,8 @@ type MemoryAnalysisCache struct {
} }
// NewMemoryAnalysisCache creates a new MemoryAnalysisCache // NewMemoryAnalysisCache creates a new MemoryAnalysisCache
func NewMemoryAnalysisCache(enabled bool) *MemoryAnalysisCache { func NewMemoryAnalysisCache(cfg *config.Config, enabled bool) *MemoryAnalysisCache {
// capacity from config cap := cfg.NLPMemoryCacheCap
cap := config.Cfg.NLPMemoryCacheCap
if cap <= 0 { if cap <= 0 {
cap = 1024 cap = 1024
} }
@ -82,13 +81,19 @@ func (c *MemoryAnalysisCache) IsEnabled() bool {
type RedisAnalysisCache struct { type RedisAnalysisCache struct {
cache cache.Cache cache cache.Cache
enabled bool enabled bool
ttl time.Duration
} }
// NewRedisAnalysisCache creates a new RedisAnalysisCache // NewRedisAnalysisCache creates a new RedisAnalysisCache
func NewRedisAnalysisCache(cache cache.Cache, enabled bool) *RedisAnalysisCache { func NewRedisAnalysisCache(cfg *config.Config, cache cache.Cache, enabled bool) *RedisAnalysisCache {
ttlSeconds := cfg.NLPRedisCacheTTLSeconds
if ttlSeconds <= 0 {
ttlSeconds = 3600 // default 1 hour
}
return &RedisAnalysisCache{ return &RedisAnalysisCache{
cache: cache, cache: cache,
enabled: enabled, enabled: enabled,
ttl: time.Duration(ttlSeconds) * time.Second,
} }
} }
@ -113,9 +118,7 @@ func (c *RedisAnalysisCache) Set(ctx context.Context, key string, result *Analys
return nil return nil
} }
// TTL from config err := c.cache.Set(ctx, key, result, c.ttl)
ttlSeconds := config.Cfg.NLPRedisCacheTTLSeconds
err := c.cache.Set(ctx, key, result, time.Duration(ttlSeconds)*time.Second)
if err != nil { if err != nil {
log.FromContext(ctx).With("key", key).Error(err, "Failed to cache analysis result") log.FromContext(ctx).With("key", key).Error(err, "Failed to cache analysis result")
return err return err

View File

@ -19,6 +19,7 @@ type LinguisticsFactory struct {
// NewLinguisticsFactory creates a new LinguisticsFactory with all components // NewLinguisticsFactory creates a new LinguisticsFactory with all components
func NewLinguisticsFactory( func NewLinguisticsFactory(
cfg *config.Config,
db *gorm.DB, db *gorm.DB,
cache cache.Cache, cache cache.Cache,
concurrency int, concurrency int,
@ -32,18 +33,18 @@ func NewLinguisticsFactory(
textAnalyzer = textAnalyzer.WithSentimentProvider(sentimentProvider) textAnalyzer = textAnalyzer.WithSentimentProvider(sentimentProvider)
// Wire language detector: lingua-go (configurable) // Wire language detector: lingua-go (configurable)
if config.Cfg.NLPUseLingua { if cfg.NLPUseLingua {
textAnalyzer = textAnalyzer.WithLanguageDetector(NewLinguaLanguageDetector()) textAnalyzer = textAnalyzer.WithLanguageDetector(NewLinguaLanguageDetector())
} }
// Wire keyword provider: lightweight TF-IDF approximation (configurable) // Wire keyword provider: lightweight TF-IDF approximation (configurable)
if config.Cfg.NLPUseTFIDF { if cfg.NLPUseTFIDF {
textAnalyzer = textAnalyzer.WithKeywordProvider(NewTFIDFKeywordProvider()) textAnalyzer = textAnalyzer.WithKeywordProvider(NewTFIDFKeywordProvider())
} }
// Create cache components // Create cache components
memoryCache := NewMemoryAnalysisCache(cacheEnabled) memoryCache := NewMemoryAnalysisCache(cfg, cacheEnabled)
redisCache := NewRedisAnalysisCache(cache, cacheEnabled) redisCache := NewRedisAnalysisCache(cfg, cache, cacheEnabled)
analysisCache := NewCompositeAnalysisCache(memoryCache, redisCache, cacheEnabled) analysisCache := NewCompositeAnalysisCache(memoryCache, redisCache, cacheEnabled)
// Create repository // Create repository

View File

@ -1,13 +1,17 @@
package linguistics package linguistics
import ( import (
"github.com/stretchr/testify/require" "tercul/internal/platform/config"
"testing" "testing"
"github.com/stretchr/testify/require"
) )
func TestFactory_WiresProviders(t *testing.T) { func TestFactory_WiresProviders(t *testing.T) {
// We won't spin a DB/cache here; this is a smoke test of wiring methods // We won't spin a DB/cache here; this is a smoke test of wiring methods
f := NewLinguisticsFactory(nil, nil, 2, true, nil) cfg, err := config.LoadConfig()
require.NoError(t, err)
f := NewLinguisticsFactory(cfg, nil, nil, 2, true, nil)
ta := f.GetTextAnalyzer().(*BasicTextAnalyzer) ta := f.GetTextAnalyzer().(*BasicTextAnalyzer)
require.NotNil(t, ta) require.NotNil(t, ta)
} }

View File

@ -17,8 +17,8 @@ type BatchProcessor struct {
} }
// NewBatchProcessor creates a new BatchProcessor // NewBatchProcessor creates a new BatchProcessor
func NewBatchProcessor(db *gorm.DB) *BatchProcessor { func NewBatchProcessor(db *gorm.DB, cfg *config.Config) *BatchProcessor {
batchSize := config.Cfg.BatchSize batchSize := cfg.BatchSize
if batchSize <= 0 { if batchSize <= 0 {
batchSize = DefaultBatchSize batchSize = DefaultBatchSize
} }

View File

@ -54,6 +54,6 @@ func (s *SyncJob) SyncEdgesBatch(ctx context.Context, batchSize, offset int) err
edgeMaps = append(edgeMaps, edgeMap) edgeMaps = append(edgeMaps, edgeMap)
} }
batchProcessor := NewBatchProcessor(s.DB) batchProcessor := NewBatchProcessor(s.DB, s.Cfg)
return batchProcessor.CreateObjectsBatch(ctx, "Edge", edgeMaps) return batchProcessor.CreateObjectsBatch(ctx, "Edge", edgeMaps)
} }

View File

@ -76,6 +76,6 @@ func (s *SyncJob) SyncAllEntities(ctx context.Context) error {
// syncEntities is a generic function to sync a given entity type. // syncEntities is a generic function to sync a given entity type.
func (s *SyncJob) syncEntities(className string, ctx context.Context) error { func (s *SyncJob) syncEntities(className string, ctx context.Context) error {
batchProcessor := NewBatchProcessor(s.DB) batchProcessor := NewBatchProcessor(s.DB, s.Cfg)
return batchProcessor.ProcessAllEntities(ctx, className) return batchProcessor.ProcessAllEntities(ctx, className)
} }

View File

@ -64,6 +64,6 @@ func RegisterQueueHandlers(srv *asynq.Server, syncJob *SyncJob) {
mux.HandleFunc(TaskEntitySync, syncJob.HandleEntitySync) mux.HandleFunc(TaskEntitySync, syncJob.HandleEntitySync)
mux.HandleFunc(TaskEdgeSync, syncJob.HandleEdgeSync) mux.HandleFunc(TaskEdgeSync, syncJob.HandleEdgeSync)
if err := srv.Run(mux); err != nil { if err := srv.Run(mux); err != nil {
log.Fatalf("Failed to start asynq server: %v", err) log.Printf("Failed to start asynq server: %v", err)
} }
} }

View File

@ -3,6 +3,7 @@ package sync
import ( import (
"context" "context"
"log" "log"
"tercul/internal/platform/config"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"gorm.io/gorm" "gorm.io/gorm"
@ -12,13 +13,15 @@ import (
type SyncJob struct { type SyncJob struct {
DB *gorm.DB DB *gorm.DB
AsynqClient *asynq.Client AsynqClient *asynq.Client
Cfg *config.Config
} }
// NewSyncJob initializes a new SyncJob. // NewSyncJob initializes a new SyncJob.
func NewSyncJob(db *gorm.DB, aClient *asynq.Client) *SyncJob { func NewSyncJob(db *gorm.DB, aClient *asynq.Client, cfg *config.Config) *SyncJob {
return &SyncJob{ return &SyncJob{
DB: db, DB: db,
AsynqClient: aClient, AsynqClient: aClient,
Cfg: cfg,
} }
} }

View File

@ -41,16 +41,17 @@ type JWTManager struct {
} }
// NewJWTManager creates a new JWT manager // NewJWTManager creates a new JWT manager
func NewJWTManager() *JWTManager { func NewJWTManager(cfg *config.Config) *JWTManager {
secretKey := config.Cfg.JWTSecret secretKey := cfg.JWTSecret
if secretKey == "" { if secretKey == "" {
secretKey = "default-secret-key-change-in-production" secretKey = "default-secret-key-change-in-production"
} }
duration := config.Cfg.JWTExpiration durationInHours := cfg.JWTExpiration
if duration == 0 { if durationInHours <= 0 {
duration = 24 * time.Hour // Default to 24 hours durationInHours = 24 // Default to 24 hours
} }
duration := time.Duration(durationInHours) * time.Hour
return &JWTManager{ return &JWTManager{
secretKey: []byte(secretKey), secretKey: []byte(secretKey),

View File

@ -54,7 +54,7 @@ func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
} }
// RoleMiddleware creates middleware for role-based authorization // RoleMiddleware creates middleware for role-based authorization
func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler { func RoleMiddleware(jwtManager *JWTManager, 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()) logger := log.FromContext(r.Context())
@ -65,7 +65,6 @@ func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
return return
} }
jwtManager := NewJWTManager()
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil { if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
logger.With("user_role", claims.Role).With("required_role", requiredRole).Warn("Authorization failed - insufficient role") logger.With("user_role", claims.Role).With("required_role", requiredRole).Warn("Authorization failed - insufficient role")
http.Error(w, "Forbidden", http.StatusForbidden) http.Error(w, "Forbidden", http.StatusForbidden)
@ -142,13 +141,12 @@ func RequireAuth(ctx context.Context) (*Claims, error) {
} }
// RequireRole ensures the user has the required role // RequireRole ensures the user has the required role
func RequireRole(ctx context.Context, requiredRole string) (*Claims, error) { func RequireRole(ctx context.Context, jwtManager *JWTManager, requiredRole string) (*Claims, error) {
claims, err := RequireAuth(ctx) claims, err := RequireAuth(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
jwtManager := NewJWTManager()
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil { if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
return nil, err return nil, err
} }

View File

@ -21,9 +21,14 @@ type Config struct {
RedisAddr string `mapstructure:"REDIS_ADDR"` RedisAddr string `mapstructure:"REDIS_ADDR"`
RedisPassword string `mapstructure:"REDIS_PASSWORD"` RedisPassword string `mapstructure:"REDIS_PASSWORD"`
RedisDB int `mapstructure:"REDIS_DB"` RedisDB int `mapstructure:"REDIS_DB"`
SyncBatchSize int `mapstructure:"SYNC_BATCH_SIZE"` BatchSize int `mapstructure:"BATCH_SIZE"`
RateLimit int `mapstructure:"RATE_LIMIT"` RateLimit int `mapstructure:"RATE_LIMIT"`
RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"` RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"`
PageSize int `mapstructure:"PAGE_SIZE"`
NLPMemoryCacheCap int `mapstructure:"NLP_MEMORY_CACHE_CAP"`
NLPRedisCacheTTLSeconds int `mapstructure:"NLP_REDIS_CACHE_TTL_SECONDS"`
NLPUseLingua bool `mapstructure:"NLP_USE_LINGUA"`
NLPUseTFIDF bool `mapstructure:"NLP_USE_TFIDF"`
} }
// LoadConfig reads configuration from file or environment variables. // LoadConfig reads configuration from file or environment variables.
@ -43,9 +48,13 @@ func LoadConfig() (*Config, error) {
viper.SetDefault("REDIS_ADDR", "localhost:6379") viper.SetDefault("REDIS_ADDR", "localhost:6379")
viper.SetDefault("REDIS_PASSWORD", "") viper.SetDefault("REDIS_PASSWORD", "")
viper.SetDefault("REDIS_DB", 0) viper.SetDefault("REDIS_DB", 0)
viper.SetDefault("SYNC_BATCH_SIZE", 100) viper.SetDefault("BATCH_SIZE", 100)
viper.SetDefault("RATE_LIMIT", 10) viper.SetDefault("RATE_LIMIT", 10)
viper.SetDefault("RATE_LIMIT_BURST", 100) viper.SetDefault("RATE_LIMIT_BURST", 100)
viper.SetDefault("NLP_MEMORY_CACHE_CAP", 1024)
viper.SetDefault("NLP_REDIS_CACHE_TTL_SECONDS", 3600)
viper.SetDefault("NLP_USE_LINGUA", true)
viper.SetDefault("NLP_USE_TFIDF", true)
viper.AutomaticEnv() viper.AutomaticEnv()

View File

@ -21,10 +21,12 @@ type RateLimiter struct {
} }
// NewRateLimiter creates a new rate limiter // NewRateLimiter creates a new rate limiter
func NewRateLimiter(rate, capacity int) *RateLimiter { func NewRateLimiter(cfg *config.Config) *RateLimiter {
rate := cfg.RateLimit
if rate <= 0 { if rate <= 0 {
rate = 10 // default rate: 10 requests per second rate = 10 // default rate: 10 requests per second
} }
capacity := cfg.RateLimitBurst
if capacity <= 0 { if capacity <= 0 {
capacity = 100 // default capacity: 100 tokens capacity = 100 // default capacity: 100 tokens
} }
@ -73,9 +75,9 @@ func minF(a, b float64) float64 {
} }
// RateLimitMiddleware creates a middleware that applies rate limiting // RateLimitMiddleware creates a middleware that applies rate limiting
func RateLimitMiddleware(next http.Handler) http.Handler { func RateLimitMiddleware(cfg *config.Config) func(http.Handler) http.Handler {
rateLimiter := NewRateLimiter(config.Cfg.RateLimit, config.Cfg.RateLimitBurst) rateLimiter := NewRateLimiter(cfg)
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) {
// Use X-Client-ID header for client identification in tests // Use X-Client-ID header for client identification in tests
clientID := r.Header.Get("X-Client-ID") clientID := r.Header.Get("X-Client-ID")
@ -98,3 +100,4 @@ func RateLimitMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
}

View File

@ -4,10 +4,9 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time"
"tercul/internal/platform/config" "tercul/internal/platform/config"
platformhttp "tercul/internal/platform/http" platformhttp "tercul/internal/platform/http"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -20,8 +19,8 @@ type RateLimiterSuite struct {
// TestRateLimiter tests the RateLimiter // TestRateLimiter tests the RateLimiter
func (s *RateLimiterSuite) TestRateLimiter() { func (s *RateLimiterSuite) TestRateLimiter() {
// Create a new rate limiter with 2 requests per second and a burst of 3 cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
limiter := platformhttp.NewRateLimiter(2, 3) limiter := platformhttp.NewRateLimiter(cfg)
// Test that the first 3 requests are allowed (burst) // Test that the first 3 requests are allowed (burst)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
@ -49,8 +48,8 @@ func (s *RateLimiterSuite) TestRateLimiter() {
// TestRateLimiterMultipleClients tests the RateLimiter with multiple clients // TestRateLimiterMultipleClients tests the RateLimiter with multiple clients
func (s *RateLimiterSuite) TestRateLimiterMultipleClients() { func (s *RateLimiterSuite) TestRateLimiterMultipleClients() {
// Create a new rate limiter with 2 requests per second and a burst of 3 cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
limiter := platformhttp.NewRateLimiter(2, 3) limiter := platformhttp.NewRateLimiter(cfg)
// Test that the first 3 requests for client1 are allowed (burst) // Test that the first 3 requests for client1 are allowed (burst)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
@ -75,17 +74,15 @@ func (s *RateLimiterSuite) TestRateLimiterMultipleClients() {
// TestRateLimiterMiddleware tests the RateLimiterMiddleware // TestRateLimiterMiddleware tests the RateLimiterMiddleware
func (s *RateLimiterSuite) TestRateLimiterMiddleware() { func (s *RateLimiterSuite) TestRateLimiterMiddleware() {
// Set config to match test expectations cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
config.Cfg.RateLimit = 2
config.Cfg.RateLimitBurst = 3
// Create a test handler that always returns 200 OK // Create a test handler that always returns 200 OK
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
}) })
// Create a rate limiter middleware with 2 requests per second and a burst of 3 // Create a rate limiter middleware
middleware := platformhttp.RateLimitMiddleware(testHandler) middleware := platformhttp.RateLimitMiddleware(cfg)(testHandler)
// Create a test server // Create a test server
server := httptest.NewServer(middleware) server := httptest.NewServer(middleware)
@ -144,22 +141,22 @@ func TestRateLimiterSuite(t *testing.T) {
// TestNewRateLimiter tests the NewRateLimiter function // TestNewRateLimiter tests the NewRateLimiter function
func TestNewRateLimiter(t *testing.T) { func TestNewRateLimiter(t *testing.T) {
// Test with valid parameters // Test with valid parameters
limiter := platformhttp.NewRateLimiter(10, 20) limiter := platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: 20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter") assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter")
// Test with zero rate (should use default) // Test with zero rate (should use default)
limiter = platformhttp.NewRateLimiter(0, 20) limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 0, RateLimitBurst: 20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate") assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate")
// Test with zero capacity (should use default) // Test with zero capacity (should use default)
limiter = platformhttp.NewRateLimiter(10, 0) limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: 0})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity") assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity")
// Test with negative rate (should use default) // Test with negative rate (should use default)
limiter = platformhttp.NewRateLimiter(-10, 20) limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: -10, RateLimitBurst: 20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate") assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate")
// Test with negative capacity (should use default) // Test with negative capacity (should use default)
limiter = platformhttp.NewRateLimiter(10, -20) limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: -20})
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity") assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity")
} }

View File

@ -11,9 +11,10 @@ import (
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/search" "tercul/internal/domain/search"
platform_auth "tercul/internal/platform/auth"
"tercul/internal/domain/work" "tercul/internal/domain/work"
"tercul/internal/jobs/linguistics" "tercul/internal/jobs/linguistics"
platform_auth "tercul/internal/platform/auth"
platform_config "tercul/internal/platform/config"
"time" "time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -106,21 +107,21 @@ func DefaultTestConfig() *TestConfig {
} }
// SetupSuite sets up the test suite with the specified configuration // SetupSuite sets up the test suite with the specified configuration
func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) { func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
if config == nil { if testConfig == nil {
config = DefaultTestConfig() testConfig = DefaultTestConfig()
} }
var dbPath string var dbPath string
if !config.UseInMemoryDB && config.DBPath != "" { if !testConfig.UseInMemoryDB && testConfig.DBPath != "" {
// Clean up previous test database file before starting // Clean up previous test database file before starting
_ = os.Remove(config.DBPath) _ = os.Remove(testConfig.DBPath)
// Ensure directory exists // Ensure directory exists
dir := filepath.Dir(config.DBPath) dir := filepath.Dir(testConfig.DBPath)
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
s.T().Fatalf("Failed to create database directory: %v", err) s.T().Fatalf("Failed to create database directory: %v", err)
} }
dbPath = config.DBPath dbPath = testConfig.DBPath
} else { } else {
// Use in-memory database // Use in-memory database
dbPath = ":memory:" dbPath = ":memory:"
@ -131,7 +132,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
log.New(os.Stdout, "\r\n", log.LstdFlags), log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{ logger.Config{
SlowThreshold: time.Second, SlowThreshold: time.Second,
LogLevel: config.LogLevel, LogLevel: testConfig.LogLevel,
IgnoreRecordNotFoundError: true, IgnoreRecordNotFoundError: true,
Colorful: false, Colorful: false,
}, },
@ -155,7 +156,12 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
&domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{}, &domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{},
) )
repos := sql.NewRepositories(s.DB) cfg, err := platform_config.LoadConfig()
if err != nil {
s.T().Fatalf("Failed to load config: %v", err)
}
repos := sql.NewRepositories(s.DB, cfg)
var searchClient search.SearchClient = &mockSearchClient{} var searchClient search.SearchClient = &mockSearchClient{}
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB) analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider() sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
@ -163,7 +169,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
s.T().Fatalf("Failed to create sentiment provider: %v", err) s.T().Fatalf("Failed to create sentiment provider: %v", err)
} }
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
jwtManager := platform_auth.NewJWTManager() jwtManager := platform_auth.NewJWTManager(cfg)
deps := app.Dependencies{ deps := app.Dependencies{
WorkRepo: repos.Work, WorkRepo: repos.Work,
@ -181,6 +187,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
SourceRepo: repos.Source, SourceRepo: repos.Source,
CopyrightRepo: repos.Copyright, CopyrightRepo: repos.Copyright,
MonetizationRepo: repos.Monetization, MonetizationRepo: repos.Monetization,
ContributionRepo: repos.Contribution,
AnalyticsRepo: repos.Analytics, AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth, AuthRepo: repos.Auth,
LocalizationRepo: repos.Localization, LocalizationRepo: repos.Localization,