From a8dfb727a1816c302fc38a2bf805babb554ee7f5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:29:18 +0000 Subject: [PATCH] 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. --- cmd/api/main.go | 124 +++++++++--------- cmd/api/server.go | 20 +-- cmd/tools/enrich/main.go | 11 +- internal/adapters/graphql/integration_test.go | 9 +- internal/adapters/graphql/schema.resolvers.go | 64 ++++++++- internal/app/analytics/service_test.go | 9 +- internal/app/app.go | 53 ++++---- internal/app/contribution/commands.go | 55 ++++++++ internal/app/contribution/service.go | 14 ++ internal/app/work/commands_test.go | 5 +- internal/data/sql/analytics_repository.go | 3 +- internal/data/sql/auth_repository.go | 3 +- internal/data/sql/author_repository.go | 5 +- internal/data/sql/author_repository_test.go | 7 +- internal/data/sql/base_repository.go | 8 +- internal/data/sql/base_repository_test.go | 13 +- internal/data/sql/book_repository.go | 5 +- internal/data/sql/book_repository_test.go | 7 +- internal/data/sql/bookmark_repository.go | 5 +- internal/data/sql/bookmark_repository_test.go | 23 +++- internal/data/sql/category_repository.go | 5 +- internal/data/sql/category_repository_test.go | 7 +- internal/data/sql/city_repository.go | 5 +- internal/data/sql/city_repository_test.go | 15 ++- internal/data/sql/collection_repository.go | 5 +- .../data/sql/collection_repository_test.go | 7 +- internal/data/sql/comment_repository.go | 5 +- internal/data/sql/comment_repository_test.go | 39 ++++-- internal/data/sql/contribution_repository.go | 5 +- .../data/sql/contribution_repository_test.go | 47 +++++-- .../data/sql/copyright_claim_repository.go | 5 +- internal/data/sql/copyright_repository.go | 5 +- .../data/sql/copyright_repository_test.go | 7 +- internal/data/sql/country_repository.go | 5 +- internal/data/sql/edge_repository.go | 5 +- internal/data/sql/edition_repository.go | 5 +- .../data/sql/email_verification_repository.go | 7 +- internal/data/sql/like_repository.go | 5 +- internal/data/sql/localization_repository.go | 3 +- internal/data/sql/monetization_repository.go | 5 +- .../data/sql/monetization_repository_test.go | 5 +- .../data/sql/password_reset_repository.go | 7 +- internal/data/sql/place_repository.go | 5 +- internal/data/sql/publisher_repository.go | 5 +- internal/data/sql/repositories.go | 41 +++--- internal/data/sql/source_repository.go | 5 +- internal/data/sql/tag_repository.go | 5 +- internal/data/sql/translation_repository.go | 5 +- internal/data/sql/user_profile_repository.go | 5 +- internal/data/sql/user_repository.go | 5 +- internal/data/sql/user_session_repository.go | 5 +- internal/data/sql/work_repository.go | 5 +- internal/data/sql/work_repository_test.go | 5 +- internal/jobs/linguistics/analysis_cache.go | 19 +-- internal/jobs/linguistics/factory.go | 11 +- internal/jobs/linguistics/factory_test.go | 10 +- internal/jobs/sync/batch_processor.go | 4 +- internal/jobs/sync/edges_sync.go | 2 +- internal/jobs/sync/entities_sync.go | 2 +- internal/jobs/sync/queue.go | 2 +- internal/jobs/sync/syncjob.go | 5 +- internal/platform/auth/jwt.go | 11 +- internal/platform/auth/middleware.go | 6 +- internal/platform/config/config.go | 47 ++++--- internal/platform/http/rate_limiter.go | 47 +++---- internal/platform/http/rate_limiter_test.go | 31 ++--- internal/testutil/integration_test_utils.go | 71 +++++----- 67 files changed, 641 insertions(+), 365 deletions(-) create mode 100644 internal/app/contribution/commands.go create mode 100644 internal/app/contribution/service.go diff --git a/cmd/api/main.go b/cmd/api/main.go index f53e32b..1ea0219 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -3,11 +3,10 @@ package main import ( "context" "fmt" + "log" "net/http" "os" "os/signal" - "path/filepath" - "runtime" "syscall" "tercul/internal/app" "tercul/internal/app/analytics" @@ -18,7 +17,7 @@ import ( "tercul/internal/platform/auth" "tercul/internal/platform/config" "tercul/internal/platform/db" - "tercul/internal/platform/log" + app_log "tercul/internal/platform/log" "tercul/internal/platform/search" "time" @@ -30,7 +29,7 @@ import ( ) // runMigrations applies database migrations using goose. -func runMigrations(gormDB *gorm.DB) error { +func runMigrations(gormDB *gorm.DB, migrationPath string) error { sqlDB, err := gormDB.DB() if err != nil { return err @@ -40,35 +39,34 @@ func runMigrations(gormDB *gorm.DB) error { return err } - // This is brittle. A better approach might be to use an env var or config. - _, b, _, _ := runtime.Caller(0) - 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 { + app_log.Info(fmt.Sprintf("Applying database migrations from %s", migrationPath)) + if err := goose.Up(sqlDB, migrationPath); err != nil { return err } - log.Info("Database migrations applied successfully") + app_log.Info("Database migrations applied successfully") return nil } // main is the entry point for the Tercul application. func main() { // Load configuration from environment variables - config.LoadConfig() + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("cannot load config: %v", err) + } // Initialize logger - log.Init("tercul-api", config.Cfg.Environment) - obsLogger := observability.NewLogger("tercul-api", config.Cfg.Environment) + app_log.Init("tercul-api", cfg.Environment) + obsLogger := observability.NewLogger("tercul-api", cfg.Environment) // Initialize OpenTelemetry Tracer Provider - tp, err := observability.TracerProvider("tercul-api", config.Cfg.Environment) + tp, err := observability.TracerProvider("tercul-api", cfg.Environment) if err != nil { - log.Fatal(err, "Failed to initialize OpenTelemetry tracer") + app_log.Fatal(err, "Failed to initialize OpenTelemetry tracer") } defer func() { 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,71 +74,72 @@ func main() { reg := prometheus.NewRegistry() 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 - database, err := db.InitDB(metrics) + database, err := db.InitDB(cfg, metrics) 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 { - log.Fatal(err, "Failed to apply database migrations") + if err := runMigrations(database, cfg.MigrationPath); err != nil { + app_log.Fatal(err, "Failed to apply database migrations") } // Initialize Weaviate client weaviateCfg := weaviate.Config{ - Host: config.Cfg.WeaviateHost, - Scheme: config.Cfg.WeaviateScheme, + Host: cfg.WeaviateHost, + Scheme: cfg.WeaviateScheme, } weaviateClient, err := weaviate.NewClient(weaviateCfg) if err != nil { - log.Fatal(err, "Failed to create weaviate client") + app_log.Fatal(err, "Failed to create weaviate client") } // Create search client searchClient := search.NewWeaviateWrapper(weaviateClient) // Create repositories - repos := dbsql.NewRepositories(database) + repos := dbsql.NewRepositories(database, cfg) // Create linguistics dependencies analysisRepo := linguistics.NewGORMAnalysisRepository(database) sentimentProvider, err := linguistics.NewGoVADERSentimentProvider() if err != nil { - log.Fatal(err, "Failed to create sentiment provider") + app_log.Fatal(err, "Failed to create sentiment provider") } // Create platform components - jwtManager := auth.NewJWTManager() + jwtManager := auth.NewJWTManager(cfg) // Create application services analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) // Create application dependencies deps := app.Dependencies{ - WorkRepo: repos.Work, - UserRepo: repos.User, - AuthorRepo: repos.Author, - TranslationRepo: repos.Translation, - CommentRepo: repos.Comment, - LikeRepo: repos.Like, - BookmarkRepo: repos.Bookmark, - CollectionRepo: repos.Collection, - TagRepo: repos.Tag, - CategoryRepo: repos.Category, - BookRepo: repos.Book, - PublisherRepo: repos.Publisher, - SourceRepo: repos.Source, - CopyrightRepo: repos.Copyright, - MonetizationRepo: repos.Monetization, - AnalyticsRepo: repos.Analytics, - AuthRepo: repos.Auth, - LocalizationRepo: repos.Localization, - SearchClient: searchClient, - AnalyticsService: analyticsService, - JWTManager: jwtManager, + WorkRepo: repos.Work, + UserRepo: repos.User, + AuthorRepo: repos.Author, + TranslationRepo: repos.Translation, + CommentRepo: repos.Comment, + LikeRepo: repos.Like, + BookmarkRepo: repos.Bookmark, + CollectionRepo: repos.Collection, + TagRepo: repos.Tag, + CategoryRepo: repos.Category, + BookRepo: repos.Book, + PublisherRepo: repos.Publisher, + SourceRepo: repos.Source, + CopyrightRepo: repos.Copyright, + MonetizationRepo: repos.Monetization, + ContributionRepo: repos.Contribution, + AnalyticsRepo: repos.Analytics, + AuthRepo: repos.Auth, + LocalizationRepo: repos.Localization, + SearchClient: searchClient, + AnalyticsService: analyticsService, + JWTManager: jwtManager, } // Create application @@ -151,28 +150,27 @@ func main() { 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) - 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.Handle("/query", apiHandler) - mux.Handle("/playground", playgroundHandler) - mux.Handle("/metrics", metricsHandler) + mux.Handle("/playground", playground.Handler("GraphQL Playground", "/query")) + mux.Handle("/metrics", observability.PrometheusHandler(reg)) - // Create a single HTTP server + // Create a single HTTP server with the main mux. mainServer := &http.Server{ - Addr: config.Cfg.ServerPort, + Addr: cfg.ServerPort, 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 go func() { 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) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - log.Info("Shutting down server...") + app_log.Info("Shutting down server...") // Graceful shutdown ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() 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") } \ No newline at end of file diff --git a/cmd/api/server.go b/cmd/api/server.go index 917153b..b7f1d79 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -9,19 +9,6 @@ import ( "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 func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager, metrics *observability.Metrics, logger *observability.Logger) http.Handler { c := graphql.Config{Resolvers: resolver} @@ -42,9 +29,6 @@ func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager, chain = observability.TracingMiddleware(chain) chain = observability.RequestIDMiddleware(chain) - // Create a mux to handle GraphQL endpoint - mux := http.NewServeMux() - mux.Handle("/query", chain) - - return mux + // Return the handler chain directly. The caller is responsible for routing. + return chain } \ No newline at end of file diff --git a/cmd/tools/enrich/main.go b/cmd/tools/enrich/main.go index afd04db..486822c 100644 --- a/cmd/tools/enrich/main.go +++ b/cmd/tools/enrich/main.go @@ -31,15 +31,18 @@ func main() { } // 2. Initialize dependencies - config.LoadConfig() + cfg, err := config.LoadConfig() + if err != nil { + log.Fatal(err, "Failed to load config") + } 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 { 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() // 3. Fetch, enrich, and save the entity diff --git a/internal/adapters/graphql/integration_test.go b/internal/adapters/graphql/integration_test.go index 584598d..2b9768d 100644 --- a/internal/adapters/graphql/integration_test.go +++ b/internal/adapters/graphql/integration_test.go @@ -20,6 +20,7 @@ import ( "tercul/internal/domain/work" "tercul/internal/observability" platform_auth "tercul/internal/platform/auth" + platform_config "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/99designs/gqlgen/graphql/handler" @@ -58,7 +59,9 @@ func (s *GraphQLIntegrationSuite) CreateAuthenticatedUser(username, email string user.Role = 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) s.Require().NoError(err) token = newToken @@ -81,7 +84,9 @@ func (s *GraphQLIntegrationSuite) SetupSuite() { srv.SetErrorPresenter(graph.NewErrorPresenter()) // 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() metrics := observability.NewMetrics(reg) diff --git a/internal/adapters/graphql/schema.resolvers.go b/internal/adapters/graphql/schema.resolvers.go index 48adac5..93fa4f6 100644 --- a/internal/adapters/graphql/schema.resolvers.go +++ b/internal/adapters/graphql/schema.resolvers.go @@ -15,6 +15,7 @@ import ( "tercul/internal/app/bookmark" "tercul/internal/app/collection" "tercul/internal/app/comment" + "tercul/internal/app/contribution" "tercul/internal/app/like" "tercul/internal/app/translation" "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. 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. @@ -1020,7 +1031,56 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool, // CreateContribution is the resolver for the createContribution field. 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. diff --git a/internal/app/analytics/service_test.go b/internal/app/analytics/service_test.go index e9bf1f2..11941da 100644 --- a/internal/app/analytics/service_test.go +++ b/internal/app/analytics/service_test.go @@ -9,6 +9,7 @@ import ( "tercul/internal/domain" "tercul/internal/domain/work" "tercul/internal/jobs/linguistics" + "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" @@ -21,10 +22,12 @@ type AnalyticsServiceTestSuite struct { func (s *AnalyticsServiceTestSuite) SetupSuite() { 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) - translationRepo := sql.NewTranslationRepository(s.DB) - workRepo := sql.NewWorkRepository(s.DB) + translationRepo := sql.NewTranslationRepository(s.DB, cfg) + workRepo := sql.NewWorkRepository(s.DB, cfg) sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider() s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, workRepo, sentimentProvider) } diff --git a/internal/app/app.go b/internal/app/app.go index d89538a..1c91e5e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,18 +2,20 @@ package app import ( "tercul/internal/app/analytics" + "tercul/internal/app/auth" "tercul/internal/app/author" + "tercul/internal/app/authz" "tercul/internal/app/book" "tercul/internal/app/bookmark" "tercul/internal/app/category" "tercul/internal/app/collection" "tercul/internal/app/comment" + "tercul/internal/app/contribution" "tercul/internal/app/like" "tercul/internal/app/localization" "tercul/internal/app/tag" "tercul/internal/app/translation" "tercul/internal/app/user" - "tercul/internal/app/auth" "tercul/internal/app/work" "tercul/internal/domain" auth_domain "tercul/internal/domain/auth" @@ -23,31 +25,30 @@ import ( platform_auth "tercul/internal/platform/auth" ) -import "tercul/internal/app/authz" - // Dependencies holds all external dependencies for the application. type Dependencies struct { - WorkRepo work_domain.WorkRepository - UserRepo domain.UserRepository - AuthorRepo domain.AuthorRepository - TranslationRepo domain.TranslationRepository - CommentRepo domain.CommentRepository - LikeRepo domain.LikeRepository - BookmarkRepo domain.BookmarkRepository - CollectionRepo domain.CollectionRepository - TagRepo domain.TagRepository - CategoryRepo domain.CategoryRepository - BookRepo domain.BookRepository - PublisherRepo domain.PublisherRepository - SourceRepo domain.SourceRepository - CopyrightRepo domain.CopyrightRepository - MonetizationRepo domain.MonetizationRepository - AnalyticsRepo analytics.Repository - AuthRepo auth_domain.AuthRepository - LocalizationRepo localization_domain.LocalizationRepository - SearchClient search.SearchClient - AnalyticsService analytics.Service - JWTManager platform_auth.JWTManagement + WorkRepo work_domain.WorkRepository + UserRepo domain.UserRepository + AuthorRepo domain.AuthorRepository + TranslationRepo domain.TranslationRepository + CommentRepo domain.CommentRepository + LikeRepo domain.LikeRepository + BookmarkRepo domain.BookmarkRepository + CollectionRepo domain.CollectionRepository + TagRepo domain.TagRepository + CategoryRepo domain.CategoryRepository + BookRepo domain.BookRepository + PublisherRepo domain.PublisherRepository + SourceRepo domain.SourceRepository + CopyrightRepo domain.CopyrightRepository + MonetizationRepo domain.MonetizationRepository + ContributionRepo domain.ContributionRepository + AnalyticsRepo analytics.Repository + AuthRepo auth_domain.AuthRepository + LocalizationRepo localization_domain.LocalizationRepository + SearchClient search.SearchClient + AnalyticsService analytics.Service + JWTManager platform_auth.JWTManagement } // Application is a container for all the application-layer services. @@ -58,6 +59,7 @@ type Application struct { Category *category.Service Collection *collection.Service Comment *comment.Service + Contribution *contribution.Service Like *like.Service Tag *tag.Service Translation *translation.Service @@ -77,6 +79,8 @@ func NewApplication(deps Dependencies) *Application { categoryService := category.NewService(deps.CategoryRepo) collectionService := collection.NewService(deps.CollectionRepo) 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) tagService := tag.NewService(deps.TagRepo) translationService := translation.NewService(deps.TranslationRepo, authzService) @@ -92,6 +96,7 @@ func NewApplication(deps Dependencies) *Application { Category: categoryService, Collection: collectionService, Comment: commentService, + Contribution: contributionService, Like: likeService, Tag: tagService, Translation: translationService, diff --git a/internal/app/contribution/commands.go b/internal/app/contribution/commands.go new file mode 100644 index 0000000..f0cb956 --- /dev/null +++ b/internal/app/contribution/commands.go @@ -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 +} \ No newline at end of file diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go new file mode 100644 index 0000000..5f9a5ec --- /dev/null +++ b/internal/app/contribution/service.go @@ -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, + } +} \ No newline at end of file diff --git a/internal/app/work/commands_test.go b/internal/app/work/commands_test.go index 5bb1387..785cb00 100644 --- a/internal/app/work/commands_test.go +++ b/internal/app/work/commands_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "tercul/internal/platform/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -172,7 +173,9 @@ func TestMergeWork_Integration(t *testing.T) { assert.NoError(t, err) // 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 searchClient := &mockSearchClient{} // Mock search client is fine commands := NewWorkCommands(workRepo, searchClient, authzSvc) diff --git a/internal/data/sql/analytics_repository.go b/internal/data/sql/analytics_repository.go index fc76ddd..4351342 100644 --- a/internal/data/sql/analytics_repository.go +++ b/internal/data/sql/analytics_repository.go @@ -6,6 +6,7 @@ import ( "tercul/internal/app/analytics" "tercul/internal/domain" "tercul/internal/domain/work" + "tercul/internal/platform/config" "time" "go.opentelemetry.io/otel" @@ -18,7 +19,7 @@ type analyticsRepository struct { tracer trace.Tracer } -func NewAnalyticsRepository(db *gorm.DB) analytics.Repository { +func NewAnalyticsRepository(db *gorm.DB, cfg *config.Config) analytics.Repository { return &analyticsRepository{ db: db, tracer: otel.Tracer("analytics.repository"), diff --git a/internal/data/sql/auth_repository.go b/internal/data/sql/auth_repository.go index 05bf859..7b5714e 100644 --- a/internal/data/sql/auth_repository.go +++ b/internal/data/sql/auth_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain/auth" + "tercul/internal/platform/config" "time" "go.opentelemetry.io/otel" @@ -15,7 +16,7 @@ type authRepository struct { tracer trace.Tracer } -func NewAuthRepository(db *gorm.DB) auth.AuthRepository { +func NewAuthRepository(db *gorm.DB, cfg *config.Config) auth.AuthRepository { return &authRepository{ db: db, tracer: otel.Tracer("auth.repository"), diff --git a/internal/data/sql/author_repository.go b/internal/data/sql/author_repository.go index b6b68d1..0661dbb 100644 --- a/internal/data/sql/author_repository.go +++ b/internal/data/sql/author_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type authorRepository struct { } // NewAuthorRepository creates a new AuthorRepository. -func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository { +func NewAuthorRepository(db *gorm.DB, cfg *config.Config) domain.AuthorRepository { return &authorRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Author](db), + BaseRepository: NewBaseRepositoryImpl[domain.Author](db, cfg), db: db, tracer: otel.Tracer("author.repository"), } diff --git a/internal/data/sql/author_repository_test.go b/internal/data/sql/author_repository_test.go index 0ec563c..767b39a 100644 --- a/internal/data/sql/author_repository_test.go +++ b/internal/data/sql/author_repository_test.go @@ -5,6 +5,7 @@ import ( "testing" "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" @@ -17,7 +18,9 @@ type AuthorRepositoryTestSuite struct { func (s *AuthorRepositoryTestSuite) SetupSuite() { 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() { @@ -58,4 +61,4 @@ func (s *AuthorRepositoryTestSuite) TestListByWorkID() { func TestAuthorRepository(t *testing.T) { suite.Run(t, new(AuthorRepositoryTestSuite)) -} +} \ No newline at end of file diff --git a/internal/data/sql/base_repository.go b/internal/data/sql/base_repository.go index 6e67215..6ddfadc 100644 --- a/internal/data/sql/base_repository.go +++ b/internal/data/sql/base_repository.go @@ -28,13 +28,15 @@ var ( type BaseRepositoryImpl[T any] struct { db *gorm.DB tracer trace.Tracer + cfg *config.Config } // 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]{ db: db, tracer: otel.Tracer("base.repository"), + cfg: cfg, } } @@ -69,7 +71,7 @@ func (r *BaseRepositoryImpl[T]) validatePagination(page, pageSize int) (int, int } if pageSize < 1 { - pageSize = config.Cfg.PageSize + pageSize = r.cfg.PageSize if pageSize < 1 { pageSize = 20 // Default page size } @@ -525,7 +527,7 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of defer span.End() if batchSize <= 0 { - batchSize = config.Cfg.BatchSize + batchSize = r.cfg.BatchSize if batchSize <= 0 { batchSize = 100 // Default batch size } diff --git a/internal/data/sql/base_repository_test.go b/internal/data/sql/base_repository_test.go index 2589caa..395e4a0 100644 --- a/internal/data/sql/base_repository_test.go +++ b/internal/data/sql/base_repository_test.go @@ -6,6 +6,7 @@ import ( "testing" "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" @@ -16,12 +17,16 @@ import ( type BaseRepositoryTestSuite struct { testutil.IntegrationTestSuite repo domain.BaseRepository[testutil.TestEntity] + cfg *config.Config } // SetupSuite initializes the test suite, database, and repository. func (s *BaseRepositoryTestSuite) SetupSuite() { 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. @@ -219,7 +224,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() { // Act err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error { 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 { return err } @@ -241,7 +246,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() { // Act err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error { 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 { return err } @@ -256,4 +261,4 @@ func (s *BaseRepositoryTestSuite) TestWithTx() { _, getErr := s.repo.GetByID(context.Background(), createdID) s.ErrorIs(getErr, sql.ErrEntityNotFound, "Entity should not exist after rollback") }) -} +} \ No newline at end of file diff --git a/internal/data/sql/book_repository.go b/internal/data/sql/book_repository.go index b0941ff..5b17765 100644 --- a/internal/data/sql/book_repository.go +++ b/internal/data/sql/book_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type bookRepository struct { } // NewBookRepository creates a new BookRepository. -func NewBookRepository(db *gorm.DB) domain.BookRepository { +func NewBookRepository(db *gorm.DB, cfg *config.Config) domain.BookRepository { return &bookRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Book](db), + BaseRepository: NewBaseRepositoryImpl[domain.Book](db, cfg), db: db, tracer: otel.Tracer("book.repository"), } diff --git a/internal/data/sql/book_repository_test.go b/internal/data/sql/book_repository_test.go index c0cc19a..bd381bf 100644 --- a/internal/data/sql/book_repository_test.go +++ b/internal/data/sql/book_repository_test.go @@ -5,6 +5,7 @@ import ( "testing" "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" @@ -17,7 +18,9 @@ type BookRepositoryTestSuite struct { func (s *BookRepositoryTestSuite) SetupSuite() { 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() { @@ -65,4 +68,4 @@ func (s *BookRepositoryTestSuite) TestFindByISBN() { func TestBookRepository(t *testing.T) { suite.Run(t, new(BookRepositoryTestSuite)) -} +} \ No newline at end of file diff --git a/internal/data/sql/bookmark_repository.go b/internal/data/sql/bookmark_repository.go index d77a2aa..06d8b3f 100644 --- a/internal/data/sql/bookmark_repository.go +++ b/internal/data/sql/bookmark_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type bookmarkRepository struct { } // NewBookmarkRepository creates a new BookmarkRepository. -func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository { +func NewBookmarkRepository(db *gorm.DB, cfg *config.Config) domain.BookmarkRepository { return &bookmarkRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db), + BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db, cfg), db: db, tracer: otel.Tracer("bookmark.repository"), } diff --git a/internal/data/sql/bookmark_repository_test.go b/internal/data/sql/bookmark_repository_test.go index d4a4e16..cf1cf73 100644 --- a/internal/data/sql/bookmark_repository_test.go +++ b/internal/data/sql/bookmark_repository_test.go @@ -7,6 +7,7 @@ import ( "testing" repo "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "github.com/DATA-DOG/go-sqlmock" @@ -17,7 +18,9 @@ import ( func TestNewBookmarkRepository(t *testing.T) { db, _, err := newMockDb() 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) } @@ -25,7 +28,9 @@ func TestBookmarkRepository_ListByUserID(t *testing.T) { t.Run("should return bookmarks for a given user id", func(t *testing.T) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewBookmarkRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewBookmarkRepository(db, cfg) userID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewBookmarkRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewBookmarkRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewBookmarkRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewBookmarkRepository(db, cfg) workID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewBookmarkRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewBookmarkRepository(db, cfg) workID := uint(1) @@ -107,4 +118,4 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) { assert.Nil(t, bookmarks) assert.NoError(t, mock.ExpectationsWereMet()) }) -} +} \ No newline at end of file diff --git a/internal/data/sql/category_repository.go b/internal/data/sql/category_repository.go index 52161f2..c656dd0 100644 --- a/internal/data/sql/category_repository.go +++ b/internal/data/sql/category_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type categoryRepository struct { } // NewCategoryRepository creates a new CategoryRepository. -func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository { +func NewCategoryRepository(db *gorm.DB, cfg *config.Config) domain.CategoryRepository { return &categoryRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Category](db), + BaseRepository: NewBaseRepositoryImpl[domain.Category](db, cfg), db: db, tracer: otel.Tracer("category.repository"), } diff --git a/internal/data/sql/category_repository_test.go b/internal/data/sql/category_repository_test.go index be9b05d..0edef8a 100644 --- a/internal/data/sql/category_repository_test.go +++ b/internal/data/sql/category_repository_test.go @@ -5,6 +5,7 @@ import ( "testing" "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" @@ -17,7 +18,9 @@ type CategoryRepositoryTestSuite struct { func (s *CategoryRepositoryTestSuite) SetupSuite() { 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() { @@ -111,4 +114,4 @@ func (s *CategoryRepositoryTestSuite) TestListByParentID() { s.Equal(parent.ID, *cat.ParentID) } }) -} +} \ No newline at end of file diff --git a/internal/data/sql/city_repository.go b/internal/data/sql/city_repository.go index e042b41..4aebc3f 100644 --- a/internal/data/sql/city_repository.go +++ b/internal/data/sql/city_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "gorm.io/gorm" ) @@ -13,9 +14,9 @@ type cityRepository struct { } // NewCityRepository creates a new CityRepository. -func NewCityRepository(db *gorm.DB) domain.CityRepository { +func NewCityRepository(db *gorm.DB, cfg *config.Config) domain.CityRepository { return &cityRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.City](db), + BaseRepository: NewBaseRepositoryImpl[domain.City](db, cfg), db: db, } } diff --git a/internal/data/sql/city_repository_test.go b/internal/data/sql/city_repository_test.go index 3dd0229..a6edf34 100644 --- a/internal/data/sql/city_repository_test.go +++ b/internal/data/sql/city_repository_test.go @@ -7,6 +7,7 @@ import ( "testing" repo "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "github.com/DATA-DOG/go-sqlmock" @@ -17,7 +18,9 @@ import ( func TestNewCityRepository(t *testing.T) { db, _, err := newMockDb() 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) } @@ -25,7 +28,9 @@ func TestCityRepository_ListByCountryID(t *testing.T) { t.Run("should return cities for a given country id", func(t *testing.T) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCityRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCityRepository(db, cfg) countryID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCityRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCityRepository(db, cfg) countryID := uint(1) @@ -63,4 +70,4 @@ func TestCityRepository_ListByCountryID(t *testing.T) { assert.Nil(t, cities) assert.NoError(t, mock.ExpectationsWereMet()) }) -} +} \ No newline at end of file diff --git a/internal/data/sql/collection_repository.go b/internal/data/sql/collection_repository.go index 4adde57..7816aa7 100644 --- a/internal/data/sql/collection_repository.go +++ b/internal/data/sql/collection_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type collectionRepository struct { } // NewCollectionRepository creates a new CollectionRepository. -func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository { +func NewCollectionRepository(db *gorm.DB, cfg *config.Config) domain.CollectionRepository { return &collectionRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Collection](db), + BaseRepository: NewBaseRepositoryImpl[domain.Collection](db, cfg), db: db, tracer: otel.Tracer("collection.repository"), } diff --git a/internal/data/sql/collection_repository_test.go b/internal/data/sql/collection_repository_test.go index 55d11f3..0e0bf6b 100644 --- a/internal/data/sql/collection_repository_test.go +++ b/internal/data/sql/collection_repository_test.go @@ -5,6 +5,7 @@ import ( "testing" "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/suite" @@ -28,7 +29,9 @@ func (s *CollectionRepositoryTestSuite) SetupTest() { s.db = gormDB 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() { @@ -101,4 +104,4 @@ func (s *CollectionRepositoryTestSuite) TestListByWorkID() { collections, err := s.repo.ListByWorkID(context.Background(), workID) s.Require().NoError(err) s.Require().Len(collections, 2) -} +} \ No newline at end of file diff --git a/internal/data/sql/comment_repository.go b/internal/data/sql/comment_repository.go index d1d3ccc..30bf063 100644 --- a/internal/data/sql/comment_repository.go +++ b/internal/data/sql/comment_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type commentRepository struct { } // NewCommentRepository creates a new CommentRepository. -func NewCommentRepository(db *gorm.DB) domain.CommentRepository { +func NewCommentRepository(db *gorm.DB, cfg *config.Config) domain.CommentRepository { return &commentRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Comment](db), + BaseRepository: NewBaseRepositoryImpl[domain.Comment](db, cfg), db: db, tracer: otel.Tracer("comment.repository"), } diff --git a/internal/data/sql/comment_repository_test.go b/internal/data/sql/comment_repository_test.go index d697383..5181e5b 100644 --- a/internal/data/sql/comment_repository_test.go +++ b/internal/data/sql/comment_repository_test.go @@ -7,6 +7,7 @@ import ( "testing" repo "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "github.com/DATA-DOG/go-sqlmock" @@ -17,7 +18,9 @@ import ( func TestNewCommentRepository(t *testing.T) { db, _, err := newMockDb() 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) } @@ -25,7 +28,9 @@ func TestCommentRepository_ListByUserID(t *testing.T) { t.Run("should return comments for a given user id", func(t *testing.T) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) userID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) workID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) translationID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) parentID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewCommentRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewCommentRepository(db, cfg) parentID := uint(1) @@ -195,4 +214,4 @@ func TestCommentRepository_ListByParentID(t *testing.T) { assert.Nil(t, comments) assert.NoError(t, mock.ExpectationsWereMet()) }) -} +} \ No newline at end of file diff --git a/internal/data/sql/contribution_repository.go b/internal/data/sql/contribution_repository.go index c2e54e5..0587081 100644 --- a/internal/data/sql/contribution_repository.go +++ b/internal/data/sql/contribution_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type contributionRepository struct { } // NewContributionRepository creates a new ContributionRepository. -func NewContributionRepository(db *gorm.DB) domain.ContributionRepository { +func NewContributionRepository(db *gorm.DB, cfg *config.Config) domain.ContributionRepository { return &contributionRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db), + BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db, cfg), db: db, tracer: otel.Tracer("contribution.repository"), } diff --git a/internal/data/sql/contribution_repository_test.go b/internal/data/sql/contribution_repository_test.go index 62f74af..dbaf5d4 100644 --- a/internal/data/sql/contribution_repository_test.go +++ b/internal/data/sql/contribution_repository_test.go @@ -7,6 +7,7 @@ import ( "testing" repo "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "github.com/DATA-DOG/go-sqlmock" @@ -17,7 +18,9 @@ import ( func TestNewContributionRepository(t *testing.T) { db, _, err := newMockDb() 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) } @@ -25,7 +28,9 @@ func TestContributionRepository_ListByUserID(t *testing.T) { t.Run("should return contributions for a given user id", func(t *testing.T) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) userID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) reviewerID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) workID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) translationID := uint(1) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) status := "draft" 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) { db, mock, err := newMockDb() require.NoError(t, err) - repo := repo.NewContributionRepository(db) + cfg, err := config.LoadConfig() + require.NoError(t, err) + repo := repo.NewContributionRepository(db, cfg) status := "draft" @@ -239,4 +262,4 @@ func TestContributionRepository_ListByStatus(t *testing.T) { assert.Nil(t, contributions) assert.NoError(t, mock.ExpectationsWereMet()) }) -} +} \ No newline at end of file diff --git a/internal/data/sql/copyright_claim_repository.go b/internal/data/sql/copyright_claim_repository.go index 1a4ec75..0a03358 100644 --- a/internal/data/sql/copyright_claim_repository.go +++ b/internal/data/sql/copyright_claim_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type copyrightClaimRepository struct { } // NewCopyrightClaimRepository creates a new CopyrightClaimRepository. -func NewCopyrightClaimRepository(db *gorm.DB) domain.CopyrightClaimRepository { +func NewCopyrightClaimRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightClaimRepository { return ©rightClaimRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db), + BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db, cfg), db: db, tracer: otel.Tracer("copyright_claim.repository"), } diff --git a/internal/data/sql/copyright_repository.go b/internal/data/sql/copyright_repository.go index 5a6d6b2..cd4a301 100644 --- a/internal/data/sql/copyright_repository.go +++ b/internal/data/sql/copyright_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type copyrightRepository struct { } // NewCopyrightRepository creates a new CopyrightRepository. -func NewCopyrightRepository(db *gorm.DB) domain.CopyrightRepository { +func NewCopyrightRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightRepository { return ©rightRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db), + BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db, cfg), db: db, tracer: otel.Tracer("copyright.repository"), } diff --git a/internal/data/sql/copyright_repository_test.go b/internal/data/sql/copyright_repository_test.go index b76a9d0..78468b7 100644 --- a/internal/data/sql/copyright_repository_test.go +++ b/internal/data/sql/copyright_repository_test.go @@ -6,6 +6,7 @@ import ( "testing" "tercul/internal/data/sql" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "github.com/DATA-DOG/go-sqlmock" @@ -41,7 +42,9 @@ func (s *CopyrightRepositoryTestSuite) SetupTest() { s.db = gormDB 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. @@ -236,4 +239,4 @@ func (s *CopyrightRepositoryTestSuite) TestRemoveCopyrightFromSource() { err := s.repo.RemoveCopyrightFromSource(context.Background(), sourceID, copyrightID) s.Require().NoError(err) }) -} +} \ No newline at end of file diff --git a/internal/data/sql/country_repository.go b/internal/data/sql/country_repository.go index 8f4f809..0c12e6d 100644 --- a/internal/data/sql/country_repository.go +++ b/internal/data/sql/country_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "gorm.io/gorm" ) @@ -14,9 +15,9 @@ type countryRepository struct { } // NewCountryRepository creates a new CountryRepository. -func NewCountryRepository(db *gorm.DB) domain.CountryRepository { +func NewCountryRepository(db *gorm.DB, cfg *config.Config) domain.CountryRepository { return &countryRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Country](db), + BaseRepository: NewBaseRepositoryImpl[domain.Country](db, cfg), db: db, } } diff --git a/internal/data/sql/edge_repository.go b/internal/data/sql/edge_repository.go index ffd8bf3..689c5bb 100644 --- a/internal/data/sql/edge_repository.go +++ b/internal/data/sql/edge_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type edgeRepository struct { } // NewEdgeRepository creates a new EdgeRepository. -func NewEdgeRepository(db *gorm.DB) domain.EdgeRepository { +func NewEdgeRepository(db *gorm.DB, cfg *config.Config) domain.EdgeRepository { return &edgeRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Edge](db), + BaseRepository: NewBaseRepositoryImpl[domain.Edge](db, cfg), db: db, tracer: otel.Tracer("edge.repository"), } diff --git a/internal/data/sql/edition_repository.go b/internal/data/sql/edition_repository.go index 54bbef1..57e28bc 100644 --- a/internal/data/sql/edition_repository.go +++ b/internal/data/sql/edition_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type editionRepository struct { } // NewEditionRepository creates a new EditionRepository. -func NewEditionRepository(db *gorm.DB) domain.EditionRepository { +func NewEditionRepository(db *gorm.DB, cfg *config.Config) domain.EditionRepository { return &editionRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Edition](db), + BaseRepository: NewBaseRepositoryImpl[domain.Edition](db, cfg), db: db, tracer: otel.Tracer("edition.repository"), } diff --git a/internal/data/sql/email_verification_repository.go b/internal/data/sql/email_verification_repository.go index 11d0da1..31d8326 100644 --- a/internal/data/sql/email_verification_repository.go +++ b/internal/data/sql/email_verification_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "go.opentelemetry.io/otel" @@ -18,9 +19,9 @@ type emailVerificationRepository struct { } // NewEmailVerificationRepository creates a new EmailVerificationRepository. -func NewEmailVerificationRepository(db *gorm.DB) domain.EmailVerificationRepository { +func NewEmailVerificationRepository(db *gorm.DB, cfg *config.Config) domain.EmailVerificationRepository { return &emailVerificationRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db), + BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db, cfg), db: db, tracer: otel.Tracer("email_verification.repository"), } @@ -69,4 +70,4 @@ func (r *emailVerificationRepository) MarkAsUsed(ctx context.Context, id uint) e return err } return nil -} +} \ No newline at end of file diff --git a/internal/data/sql/like_repository.go b/internal/data/sql/like_repository.go index 71e38c8..6796a4f 100644 --- a/internal/data/sql/like_repository.go +++ b/internal/data/sql/like_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type likeRepository struct { } // NewLikeRepository creates a new LikeRepository. -func NewLikeRepository(db *gorm.DB) domain.LikeRepository { +func NewLikeRepository(db *gorm.DB, cfg *config.Config) domain.LikeRepository { return &likeRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Like](db), + BaseRepository: NewBaseRepositoryImpl[domain.Like](db, cfg), db: db, tracer: otel.Tracer("like.repository"), } diff --git a/internal/data/sql/localization_repository.go b/internal/data/sql/localization_repository.go index 5babeda..53b04aa 100644 --- a/internal/data/sql/localization_repository.go +++ b/internal/data/sql/localization_repository.go @@ -4,6 +4,7 @@ import ( "context" "tercul/internal/domain" "tercul/internal/domain/localization" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -15,7 +16,7 @@ type localizationRepository struct { tracer trace.Tracer } -func NewLocalizationRepository(db *gorm.DB) localization.LocalizationRepository { +func NewLocalizationRepository(db *gorm.DB, cfg *config.Config) localization.LocalizationRepository { return &localizationRepository{ db: db, tracer: otel.Tracer("localization.repository"), diff --git a/internal/data/sql/monetization_repository.go b/internal/data/sql/monetization_repository.go index fe5f226..70caff8 100644 --- a/internal/data/sql/monetization_repository.go +++ b/internal/data/sql/monetization_repository.go @@ -4,6 +4,7 @@ import ( "context" "tercul/internal/domain" "tercul/internal/domain/work" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type monetizationRepository struct { } // NewMonetizationRepository creates a new MonetizationRepository. -func NewMonetizationRepository(db *gorm.DB) domain.MonetizationRepository { +func NewMonetizationRepository(db *gorm.DB, cfg *config.Config) domain.MonetizationRepository { return &monetizationRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db), + BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db, cfg), db: db, tracer: otel.Tracer("monetization.repository"), } diff --git a/internal/data/sql/monetization_repository_test.go b/internal/data/sql/monetization_repository_test.go index 3fc24dc..c50f21c 100644 --- a/internal/data/sql/monetization_repository_test.go +++ b/internal/data/sql/monetization_repository_test.go @@ -6,6 +6,7 @@ import ( "tercul/internal/data/sql" "tercul/internal/domain" workdomain "tercul/internal/domain/work" + "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" @@ -18,7 +19,9 @@ type MonetizationRepositoryTestSuite struct { func (s *MonetizationRepositoryTestSuite) SetupSuite() { 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() { diff --git a/internal/data/sql/password_reset_repository.go b/internal/data/sql/password_reset_repository.go index 631b0af..6a81174 100644 --- a/internal/data/sql/password_reset_repository.go +++ b/internal/data/sql/password_reset_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "go.opentelemetry.io/otel" @@ -18,9 +19,9 @@ type passwordResetRepository struct { } // NewPasswordResetRepository creates a new PasswordResetRepository. -func NewPasswordResetRepository(db *gorm.DB) domain.PasswordResetRepository { +func NewPasswordResetRepository(db *gorm.DB, cfg *config.Config) domain.PasswordResetRepository { return &passwordResetRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db), + BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db, cfg), db: db, tracer: otel.Tracer("password_reset.repository"), } @@ -69,4 +70,4 @@ func (r *passwordResetRepository) MarkAsUsed(ctx context.Context, id uint) error return err } return nil -} +} \ No newline at end of file diff --git a/internal/data/sql/place_repository.go b/internal/data/sql/place_repository.go index 0a78d4e..14cb004 100644 --- a/internal/data/sql/place_repository.go +++ b/internal/data/sql/place_repository.go @@ -4,6 +4,7 @@ import ( "context" "math" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type placeRepository struct { } // NewPlaceRepository creates a new PlaceRepository. -func NewPlaceRepository(db *gorm.DB) domain.PlaceRepository { +func NewPlaceRepository(db *gorm.DB, cfg *config.Config) domain.PlaceRepository { return &placeRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Place](db), + BaseRepository: NewBaseRepositoryImpl[domain.Place](db, cfg), db: db, tracer: otel.Tracer("place.repository"), } diff --git a/internal/data/sql/publisher_repository.go b/internal/data/sql/publisher_repository.go index ade696b..778beee 100644 --- a/internal/data/sql/publisher_repository.go +++ b/internal/data/sql/publisher_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -16,9 +17,9 @@ type publisherRepository struct { } // NewPublisherRepository creates a new PublisherRepository. -func NewPublisherRepository(db *gorm.DB) domain.PublisherRepository { +func NewPublisherRepository(db *gorm.DB, cfg *config.Config) domain.PublisherRepository { return &publisherRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db), + BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db, cfg), db: db, tracer: otel.Tracer("publisher.repository"), } diff --git a/internal/data/sql/repositories.go b/internal/data/sql/repositories.go index ceaf8c6..66c8bde 100644 --- a/internal/data/sql/repositories.go +++ b/internal/data/sql/repositories.go @@ -6,6 +6,7 @@ import ( "tercul/internal/domain/auth" "tercul/internal/domain/localization" "tercul/internal/domain/work" + "tercul/internal/platform/config" "gorm.io/gorm" ) @@ -26,31 +27,33 @@ type Repositories struct { Source domain.SourceRepository Copyright domain.CopyrightRepository Monetization domain.MonetizationRepository + Contribution domain.ContributionRepository Analytics analytics.Repository Auth auth.AuthRepository Localization localization.LocalizationRepository } // NewRepositories creates a new Repositories container -func NewRepositories(db *gorm.DB) *Repositories { +func NewRepositories(db *gorm.DB, cfg *config.Config) *Repositories { return &Repositories{ - Work: NewWorkRepository(db), - User: NewUserRepository(db), - Author: NewAuthorRepository(db), - Translation: NewTranslationRepository(db), - Comment: NewCommentRepository(db), - Like: NewLikeRepository(db), - Bookmark: NewBookmarkRepository(db), - Collection: NewCollectionRepository(db), - Tag: NewTagRepository(db), - Category: NewCategoryRepository(db), - Book: NewBookRepository(db), - Publisher: NewPublisherRepository(db), - Source: NewSourceRepository(db), - Copyright: NewCopyrightRepository(db), - Monetization: NewMonetizationRepository(db), - Analytics: NewAnalyticsRepository(db), - Auth: NewAuthRepository(db), - Localization: NewLocalizationRepository(db), + Work: NewWorkRepository(db, cfg), + User: NewUserRepository(db, cfg), + Author: NewAuthorRepository(db, cfg), + Translation: NewTranslationRepository(db, cfg), + Comment: NewCommentRepository(db, cfg), + Like: NewLikeRepository(db, cfg), + Bookmark: NewBookmarkRepository(db, cfg), + Collection: NewCollectionRepository(db, cfg), + Tag: NewTagRepository(db, cfg), + Category: NewCategoryRepository(db, cfg), + Book: NewBookRepository(db, cfg), + Publisher: NewPublisherRepository(db, cfg), + Source: NewSourceRepository(db, cfg), + Copyright: NewCopyrightRepository(db, cfg), + Monetization: NewMonetizationRepository(db, cfg), + Contribution: NewContributionRepository(db, cfg), + Analytics: NewAnalyticsRepository(db, cfg), + Auth: NewAuthRepository(db, cfg), + Localization: NewLocalizationRepository(db, cfg), } } diff --git a/internal/data/sql/source_repository.go b/internal/data/sql/source_repository.go index b3715be..4702c8c 100644 --- a/internal/data/sql/source_repository.go +++ b/internal/data/sql/source_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type sourceRepository struct { } // NewSourceRepository creates a new SourceRepository. -func NewSourceRepository(db *gorm.DB) domain.SourceRepository { +func NewSourceRepository(db *gorm.DB, cfg *config.Config) domain.SourceRepository { return &sourceRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Source](db), + BaseRepository: NewBaseRepositoryImpl[domain.Source](db, cfg), db: db, tracer: otel.Tracer("source.repository"), } diff --git a/internal/data/sql/tag_repository.go b/internal/data/sql/tag_repository.go index fae14ce..f61e975 100644 --- a/internal/data/sql/tag_repository.go +++ b/internal/data/sql/tag_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type tagRepository struct { } // NewTagRepository creates a new TagRepository. -func NewTagRepository(db *gorm.DB) domain.TagRepository { +func NewTagRepository(db *gorm.DB, cfg *config.Config) domain.TagRepository { return &tagRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Tag](db), + BaseRepository: NewBaseRepositoryImpl[domain.Tag](db, cfg), db: db, tracer: otel.Tracer("tag.repository"), } diff --git a/internal/data/sql/translation_repository.go b/internal/data/sql/translation_repository.go index 8b97139..14ef37a 100644 --- a/internal/data/sql/translation_repository.go +++ b/internal/data/sql/translation_repository.go @@ -3,6 +3,7 @@ package sql import ( "context" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type translationRepository struct { } // NewTranslationRepository creates a new TranslationRepository. -func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository { +func NewTranslationRepository(db *gorm.DB, cfg *config.Config) domain.TranslationRepository { return &translationRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.Translation](db), + BaseRepository: NewBaseRepositoryImpl[domain.Translation](db, cfg), db: db, tracer: otel.Tracer("translation.repository"), } diff --git a/internal/data/sql/user_profile_repository.go b/internal/data/sql/user_profile_repository.go index 9dd3e23..351adeb 100644 --- a/internal/data/sql/user_profile_repository.go +++ b/internal/data/sql/user_profile_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type userProfileRepository struct { } // NewUserProfileRepository creates a new UserProfileRepository. -func NewUserProfileRepository(db *gorm.DB) domain.UserProfileRepository { +func NewUserProfileRepository(db *gorm.DB, cfg *config.Config) domain.UserProfileRepository { return &userProfileRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db), + BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db, cfg), db: db, tracer: otel.Tracer("user_profile.repository"), } diff --git a/internal/data/sql/user_repository.go b/internal/data/sql/user_repository.go index 816f759..a6bed79 100644 --- a/internal/data/sql/user_repository.go +++ b/internal/data/sql/user_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -17,9 +18,9 @@ type userRepository struct { } // NewUserRepository creates a new UserRepository. -func NewUserRepository(db *gorm.DB) domain.UserRepository { +func NewUserRepository(db *gorm.DB, cfg *config.Config) domain.UserRepository { return &userRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.User](db), + BaseRepository: NewBaseRepositoryImpl[domain.User](db, cfg), db: db, tracer: otel.Tracer("user.repository"), } diff --git a/internal/data/sql/user_session_repository.go b/internal/data/sql/user_session_repository.go index fbd671f..a431822 100644 --- a/internal/data/sql/user_session_repository.go +++ b/internal/data/sql/user_session_repository.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/config" "time" "go.opentelemetry.io/otel" @@ -18,9 +19,9 @@ type userSessionRepository struct { } // NewUserSessionRepository creates a new UserSessionRepository. -func NewUserSessionRepository(db *gorm.DB) domain.UserSessionRepository { +func NewUserSessionRepository(db *gorm.DB, cfg *config.Config) domain.UserSessionRepository { return &userSessionRepository{ - BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db), + BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db, cfg), db: db, tracer: otel.Tracer("user_session.repository"), } diff --git a/internal/data/sql/work_repository.go b/internal/data/sql/work_repository.go index c6e66ba..d0dbc91 100644 --- a/internal/data/sql/work_repository.go +++ b/internal/data/sql/work_repository.go @@ -6,6 +6,7 @@ import ( "fmt" "tercul/internal/domain" "tercul/internal/domain/work" + "tercul/internal/platform/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -19,9 +20,9 @@ type workRepository struct { } // NewWorkRepository creates a new WorkRepository. -func NewWorkRepository(db *gorm.DB) work.WorkRepository { +func NewWorkRepository(db *gorm.DB, cfg *config.Config) work.WorkRepository { return &workRepository{ - BaseRepository: NewBaseRepositoryImpl[work.Work](db), + BaseRepository: NewBaseRepositoryImpl[work.Work](db, cfg), db: db, tracer: otel.Tracer("work.repository"), } diff --git a/internal/data/sql/work_repository_test.go b/internal/data/sql/work_repository_test.go index 36800dd..1928c6f 100644 --- a/internal/data/sql/work_repository_test.go +++ b/internal/data/sql/work_repository_test.go @@ -6,6 +6,7 @@ import ( "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/domain/work" + "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" @@ -18,7 +19,9 @@ type WorkRepositoryTestSuite struct { func (s *WorkRepositoryTestSuite) SetupSuite() { 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() { diff --git a/internal/jobs/linguistics/analysis_cache.go b/internal/jobs/linguistics/analysis_cache.go index 9f3bdf1..6100127 100644 --- a/internal/jobs/linguistics/analysis_cache.go +++ b/internal/jobs/linguistics/analysis_cache.go @@ -31,9 +31,8 @@ type MemoryAnalysisCache struct { } // NewMemoryAnalysisCache creates a new MemoryAnalysisCache -func NewMemoryAnalysisCache(enabled bool) *MemoryAnalysisCache { - // capacity from config - cap := config.Cfg.NLPMemoryCacheCap +func NewMemoryAnalysisCache(cfg *config.Config, enabled bool) *MemoryAnalysisCache { + cap := cfg.NLPMemoryCacheCap if cap <= 0 { cap = 1024 } @@ -82,13 +81,19 @@ func (c *MemoryAnalysisCache) IsEnabled() bool { type RedisAnalysisCache struct { cache cache.Cache enabled bool + ttl time.Duration } // 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{ cache: cache, 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 } - // TTL from config - ttlSeconds := config.Cfg.NLPRedisCacheTTLSeconds - err := c.cache.Set(ctx, key, result, time.Duration(ttlSeconds)*time.Second) + err := c.cache.Set(ctx, key, result, c.ttl) if err != nil { log.FromContext(ctx).With("key", key).Error(err, "Failed to cache analysis result") return err @@ -189,4 +192,4 @@ func (c *CompositeAnalysisCache) Set(ctx context.Context, key string, result *An // IsEnabled returns whether caching is enabled func (c *CompositeAnalysisCache) IsEnabled() bool { return c.enabled -} +} \ No newline at end of file diff --git a/internal/jobs/linguistics/factory.go b/internal/jobs/linguistics/factory.go index 69c25a1..9f5c343 100644 --- a/internal/jobs/linguistics/factory.go +++ b/internal/jobs/linguistics/factory.go @@ -19,6 +19,7 @@ type LinguisticsFactory struct { // NewLinguisticsFactory creates a new LinguisticsFactory with all components func NewLinguisticsFactory( + cfg *config.Config, db *gorm.DB, cache cache.Cache, concurrency int, @@ -32,18 +33,18 @@ func NewLinguisticsFactory( textAnalyzer = textAnalyzer.WithSentimentProvider(sentimentProvider) // Wire language detector: lingua-go (configurable) - if config.Cfg.NLPUseLingua { + if cfg.NLPUseLingua { textAnalyzer = textAnalyzer.WithLanguageDetector(NewLinguaLanguageDetector()) } // Wire keyword provider: lightweight TF-IDF approximation (configurable) - if config.Cfg.NLPUseTFIDF { + if cfg.NLPUseTFIDF { textAnalyzer = textAnalyzer.WithKeywordProvider(NewTFIDFKeywordProvider()) } // Create cache components - memoryCache := NewMemoryAnalysisCache(cacheEnabled) - redisCache := NewRedisAnalysisCache(cache, cacheEnabled) + memoryCache := NewMemoryAnalysisCache(cfg, cacheEnabled) + redisCache := NewRedisAnalysisCache(cfg, cache, cacheEnabled) analysisCache := NewCompositeAnalysisCache(memoryCache, redisCache, cacheEnabled) // Create repository @@ -105,4 +106,4 @@ func (f *LinguisticsFactory) GetAnalyzer() Analyzer { // GetSentimentProvider returns the sentiment provider func (f *LinguisticsFactory) GetSentimentProvider() SentimentProvider { return f.sentimentProvider -} +} \ No newline at end of file diff --git a/internal/jobs/linguistics/factory_test.go b/internal/jobs/linguistics/factory_test.go index 65939d9..dea7d00 100644 --- a/internal/jobs/linguistics/factory_test.go +++ b/internal/jobs/linguistics/factory_test.go @@ -1,13 +1,17 @@ package linguistics import ( - "github.com/stretchr/testify/require" + "tercul/internal/platform/config" "testing" + + "github.com/stretchr/testify/require" ) func TestFactory_WiresProviders(t *testing.T) { // 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) require.NotNil(t, ta) -} +} \ No newline at end of file diff --git a/internal/jobs/sync/batch_processor.go b/internal/jobs/sync/batch_processor.go index bbd3e78..f811d28 100644 --- a/internal/jobs/sync/batch_processor.go +++ b/internal/jobs/sync/batch_processor.go @@ -17,8 +17,8 @@ type BatchProcessor struct { } // NewBatchProcessor creates a new BatchProcessor -func NewBatchProcessor(db *gorm.DB) *BatchProcessor { - batchSize := config.Cfg.BatchSize +func NewBatchProcessor(db *gorm.DB, cfg *config.Config) *BatchProcessor { + batchSize := cfg.BatchSize if batchSize <= 0 { batchSize = DefaultBatchSize } diff --git a/internal/jobs/sync/edges_sync.go b/internal/jobs/sync/edges_sync.go index 780b089..0519475 100644 --- a/internal/jobs/sync/edges_sync.go +++ b/internal/jobs/sync/edges_sync.go @@ -54,6 +54,6 @@ func (s *SyncJob) SyncEdgesBatch(ctx context.Context, batchSize, offset int) err edgeMaps = append(edgeMaps, edgeMap) } - batchProcessor := NewBatchProcessor(s.DB) + batchProcessor := NewBatchProcessor(s.DB, s.Cfg) return batchProcessor.CreateObjectsBatch(ctx, "Edge", edgeMaps) } diff --git a/internal/jobs/sync/entities_sync.go b/internal/jobs/sync/entities_sync.go index 96034c8..1ce4245 100644 --- a/internal/jobs/sync/entities_sync.go +++ b/internal/jobs/sync/entities_sync.go @@ -76,6 +76,6 @@ func (s *SyncJob) SyncAllEntities(ctx context.Context) error { // syncEntities is a generic function to sync a given entity type. 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) } diff --git a/internal/jobs/sync/queue.go b/internal/jobs/sync/queue.go index 361d5df..d010709 100644 --- a/internal/jobs/sync/queue.go +++ b/internal/jobs/sync/queue.go @@ -64,6 +64,6 @@ func RegisterQueueHandlers(srv *asynq.Server, syncJob *SyncJob) { mux.HandleFunc(TaskEntitySync, syncJob.HandleEntitySync) mux.HandleFunc(TaskEdgeSync, syncJob.HandleEdgeSync) 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) } } diff --git a/internal/jobs/sync/syncjob.go b/internal/jobs/sync/syncjob.go index 34eefc8..66acef8 100644 --- a/internal/jobs/sync/syncjob.go +++ b/internal/jobs/sync/syncjob.go @@ -3,6 +3,7 @@ package sync import ( "context" "log" + "tercul/internal/platform/config" "github.com/hibiken/asynq" "gorm.io/gorm" @@ -12,13 +13,15 @@ import ( type SyncJob struct { DB *gorm.DB AsynqClient *asynq.Client + Cfg *config.Config } // 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{ DB: db, AsynqClient: aClient, + Cfg: cfg, } } diff --git a/internal/platform/auth/jwt.go b/internal/platform/auth/jwt.go index 1738cf3..554a82c 100644 --- a/internal/platform/auth/jwt.go +++ b/internal/platform/auth/jwt.go @@ -41,16 +41,17 @@ type JWTManager struct { } // NewJWTManager creates a new JWT manager -func NewJWTManager() *JWTManager { - secretKey := config.Cfg.JWTSecret +func NewJWTManager(cfg *config.Config) *JWTManager { + secretKey := cfg.JWTSecret if secretKey == "" { secretKey = "default-secret-key-change-in-production" } - duration := config.Cfg.JWTExpiration - if duration == 0 { - duration = 24 * time.Hour // Default to 24 hours + durationInHours := cfg.JWTExpiration + if durationInHours <= 0 { + durationInHours = 24 // Default to 24 hours } + duration := time.Duration(durationInHours) * time.Hour return &JWTManager{ secretKey: []byte(secretKey), diff --git a/internal/platform/auth/middleware.go b/internal/platform/auth/middleware.go index c80e64f..809b329 100644 --- a/internal/platform/auth/middleware.go +++ b/internal/platform/auth/middleware.go @@ -54,7 +54,7 @@ func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler { } // 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 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := log.FromContext(r.Context()) @@ -65,7 +65,6 @@ func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler { return } - jwtManager := NewJWTManager() if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil { logger.With("user_role", claims.Role).With("required_role", requiredRole).Warn("Authorization failed - insufficient role") 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 -func RequireRole(ctx context.Context, requiredRole string) (*Claims, error) { +func RequireRole(ctx context.Context, jwtManager *JWTManager, requiredRole string) (*Claims, error) { claims, err := RequireAuth(ctx) if err != nil { return nil, err } - jwtManager := NewJWTManager() if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil { return nil, err } diff --git a/internal/platform/config/config.go b/internal/platform/config/config.go index 9367df2..194478b 100644 --- a/internal/platform/config/config.go +++ b/internal/platform/config/config.go @@ -6,24 +6,29 @@ import ( // Config stores all configuration of the application. type Config struct { - Environment string `mapstructure:"ENVIRONMENT"` - ServerPort string `mapstructure:"SERVER_PORT"` - DBHost string `mapstructure:"DB_HOST"` - DBPort string `mapstructure:"DB_PORT"` - DBUser string `mapstructure:"DB_USER"` - DBPassword string `mapstructure:"DB_PASSWORD"` - DBName string `mapstructure:"DB_NAME"` - JWTSecret string `mapstructure:"JWT_SECRET"` - JWTExpiration int `mapstructure:"JWT_EXPIRATION_HOURS"` - WeaviateHost string `mapstructure:"WEAVIATE_HOST"` - WeaviateScheme string `mapstructure:"WEAVIATE_SCHEME"` - MigrationPath string `mapstructure:"MIGRATION_PATH"` - RedisAddr string `mapstructure:"REDIS_ADDR"` - RedisPassword string `mapstructure:"REDIS_PASSWORD"` - RedisDB int `mapstructure:"REDIS_DB"` - SyncBatchSize int `mapstructure:"SYNC_BATCH_SIZE"` - RateLimit int `mapstructure:"RATE_LIMIT"` - RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"` + Environment string `mapstructure:"ENVIRONMENT"` + ServerPort string `mapstructure:"SERVER_PORT"` + DBHost string `mapstructure:"DB_HOST"` + DBPort string `mapstructure:"DB_PORT"` + DBUser string `mapstructure:"DB_USER"` + DBPassword string `mapstructure:"DB_PASSWORD"` + DBName string `mapstructure:"DB_NAME"` + JWTSecret string `mapstructure:"JWT_SECRET"` + JWTExpiration int `mapstructure:"JWT_EXPIRATION_HOURS"` + WeaviateHost string `mapstructure:"WEAVIATE_HOST"` + WeaviateScheme string `mapstructure:"WEAVIATE_SCHEME"` + MigrationPath string `mapstructure:"MIGRATION_PATH"` + RedisAddr string `mapstructure:"REDIS_ADDR"` + RedisPassword string `mapstructure:"REDIS_PASSWORD"` + RedisDB int `mapstructure:"REDIS_DB"` + BatchSize int `mapstructure:"BATCH_SIZE"` + RateLimit int `mapstructure:"RATE_LIMIT"` + 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. @@ -43,9 +48,13 @@ func LoadConfig() (*Config, error) { viper.SetDefault("REDIS_ADDR", "localhost:6379") viper.SetDefault("REDIS_PASSWORD", "") 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_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() diff --git a/internal/platform/http/rate_limiter.go b/internal/platform/http/rate_limiter.go index ed3cb6f..4694e57 100644 --- a/internal/platform/http/rate_limiter.go +++ b/internal/platform/http/rate_limiter.go @@ -21,10 +21,12 @@ type RateLimiter struct { } // NewRateLimiter creates a new rate limiter -func NewRateLimiter(rate, capacity int) *RateLimiter { +func NewRateLimiter(cfg *config.Config) *RateLimiter { + rate := cfg.RateLimit if rate <= 0 { rate = 10 // default rate: 10 requests per second } + capacity := cfg.RateLimitBurst if capacity <= 0 { capacity = 100 // default capacity: 100 tokens } @@ -73,28 +75,29 @@ func minF(a, b float64) float64 { } // RateLimitMiddleware creates a middleware that applies rate limiting -func RateLimitMiddleware(next http.Handler) http.Handler { - rateLimiter := NewRateLimiter(config.Cfg.RateLimit, config.Cfg.RateLimitBurst) +func RateLimitMiddleware(cfg *config.Config) func(http.Handler) http.Handler { + rateLimiter := NewRateLimiter(cfg) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Use X-Client-ID header for client identification in tests + clientID := r.Header.Get("X-Client-ID") + if clientID == "" { + clientID = r.RemoteAddr + } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Use X-Client-ID header for client identification in tests - clientID := r.Header.Get("X-Client-ID") - if clientID == "" { - clientID = r.RemoteAddr - } + // Check if request is allowed + if !rateLimiter.Allow(clientID) { + log.FromContext(r.Context()). + With("clientID", clientID). + Warn("Rate limit exceeded") - // Check if request is allowed - if !rateLimiter.Allow(clientID) { - log.FromContext(r.Context()). - With("clientID", clientID). - Warn("Rate limit exceeded") + w.WriteHeader(http.StatusTooManyRequests) + w.Write([]byte("Rate limit exceeded. Please try again later.")) + return + } - w.WriteHeader(http.StatusTooManyRequests) - w.Write([]byte("Rate limit exceeded. Please try again later.")) - return - } - - // Continue to the next handler - next.ServeHTTP(w, r) - }) + // Continue to the next handler + next.ServeHTTP(w, r) + }) + } } diff --git a/internal/platform/http/rate_limiter_test.go b/internal/platform/http/rate_limiter_test.go index cfbb9e5..61ac83a 100644 --- a/internal/platform/http/rate_limiter_test.go +++ b/internal/platform/http/rate_limiter_test.go @@ -4,10 +4,9 @@ import ( "net/http" "net/http/httptest" "testing" - "time" - "tercul/internal/platform/config" platformhttp "tercul/internal/platform/http" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -20,8 +19,8 @@ type RateLimiterSuite struct { // TestRateLimiter tests the RateLimiter func (s *RateLimiterSuite) TestRateLimiter() { - // Create a new rate limiter with 2 requests per second and a burst of 3 - limiter := platformhttp.NewRateLimiter(2, 3) + cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3} + limiter := platformhttp.NewRateLimiter(cfg) // Test that the first 3 requests are allowed (burst) for i := 0; i < 3; i++ { @@ -49,8 +48,8 @@ func (s *RateLimiterSuite) TestRateLimiter() { // TestRateLimiterMultipleClients tests the RateLimiter with multiple clients func (s *RateLimiterSuite) TestRateLimiterMultipleClients() { - // Create a new rate limiter with 2 requests per second and a burst of 3 - limiter := platformhttp.NewRateLimiter(2, 3) + cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3} + limiter := platformhttp.NewRateLimiter(cfg) // Test that the first 3 requests for client1 are allowed (burst) for i := 0; i < 3; i++ { @@ -75,17 +74,15 @@ func (s *RateLimiterSuite) TestRateLimiterMultipleClients() { // TestRateLimiterMiddleware tests the RateLimiterMiddleware func (s *RateLimiterSuite) TestRateLimiterMiddleware() { - // Set config to match test expectations - config.Cfg.RateLimit = 2 - config.Cfg.RateLimitBurst = 3 + cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3} // Create a test handler that always returns 200 OK testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) - // Create a rate limiter middleware with 2 requests per second and a burst of 3 - middleware := platformhttp.RateLimitMiddleware(testHandler) + // Create a rate limiter middleware + middleware := platformhttp.RateLimitMiddleware(cfg)(testHandler) // Create a test server server := httptest.NewServer(middleware) @@ -144,22 +141,22 @@ func TestRateLimiterSuite(t *testing.T) { // TestNewRateLimiter tests the NewRateLimiter function func TestNewRateLimiter(t *testing.T) { // 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") // 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") // 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") // 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") // 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") -} +} \ No newline at end of file diff --git a/internal/testutil/integration_test_utils.go b/internal/testutil/integration_test_utils.go index a159be2..89f5cc1 100644 --- a/internal/testutil/integration_test_utils.go +++ b/internal/testutil/integration_test_utils.go @@ -11,9 +11,10 @@ import ( "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/domain/search" - platform_auth "tercul/internal/platform/auth" "tercul/internal/domain/work" "tercul/internal/jobs/linguistics" + platform_auth "tercul/internal/platform/auth" + platform_config "tercul/internal/platform/config" "time" "github.com/stretchr/testify/suite" @@ -106,21 +107,21 @@ func DefaultTestConfig() *TestConfig { } // SetupSuite sets up the test suite with the specified configuration -func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) { - if config == nil { - config = DefaultTestConfig() +func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) { + if testConfig == nil { + testConfig = DefaultTestConfig() } var dbPath string - if !config.UseInMemoryDB && config.DBPath != "" { + if !testConfig.UseInMemoryDB && testConfig.DBPath != "" { // Clean up previous test database file before starting - _ = os.Remove(config.DBPath) + _ = os.Remove(testConfig.DBPath) // Ensure directory exists - dir := filepath.Dir(config.DBPath) + dir := filepath.Dir(testConfig.DBPath) if err := os.MkdirAll(dir, 0755); err != nil { s.T().Fatalf("Failed to create database directory: %v", err) } - dbPath = config.DBPath + dbPath = testConfig.DBPath } else { // Use in-memory database dbPath = ":memory:" @@ -131,7 +132,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) { log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: time.Second, - LogLevel: config.LogLevel, + LogLevel: testConfig.LogLevel, IgnoreRecordNotFoundError: true, Colorful: false, }, @@ -155,7 +156,12 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) { &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{} analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB) sentimentProvider, err := linguistics.NewGoVADERSentimentProvider() @@ -163,30 +169,31 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) { s.T().Fatalf("Failed to create sentiment provider: %v", err) } analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) - jwtManager := platform_auth.NewJWTManager() + jwtManager := platform_auth.NewJWTManager(cfg) deps := app.Dependencies{ - WorkRepo: repos.Work, - UserRepo: repos.User, - AuthorRepo: repos.Author, - TranslationRepo: repos.Translation, - CommentRepo: repos.Comment, - LikeRepo: repos.Like, - BookmarkRepo: repos.Bookmark, - CollectionRepo: repos.Collection, - TagRepo: repos.Tag, - CategoryRepo: repos.Category, - BookRepo: repos.Book, - PublisherRepo: repos.Publisher, - SourceRepo: repos.Source, - CopyrightRepo: repos.Copyright, - MonetizationRepo: repos.Monetization, - AnalyticsRepo: repos.Analytics, - AuthRepo: repos.Auth, - LocalizationRepo: repos.Localization, - SearchClient: searchClient, - AnalyticsService: analyticsService, - JWTManager: jwtManager, + WorkRepo: repos.Work, + UserRepo: repos.User, + AuthorRepo: repos.Author, + TranslationRepo: repos.Translation, + CommentRepo: repos.Comment, + LikeRepo: repos.Like, + BookmarkRepo: repos.Bookmark, + CollectionRepo: repos.Collection, + TagRepo: repos.Tag, + CategoryRepo: repos.Category, + BookRepo: repos.Book, + PublisherRepo: repos.Publisher, + SourceRepo: repos.Source, + CopyrightRepo: repos.Copyright, + MonetizationRepo: repos.Monetization, + ContributionRepo: repos.Contribution, + AnalyticsRepo: repos.Analytics, + AuthRepo: repos.Auth, + LocalizationRepo: repos.Localization, + SearchClient: searchClient, + AnalyticsService: analyticsService, + JWTManager: jwtManager, } s.App = app.NewApplication(deps)