feat: Implement critical features and fix build

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

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

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

View File

@ -3,11 +3,10 @@ package main
import (
"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")
}

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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)
}

View File

@ -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,

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import (
"context"
"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)

View File

@ -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"),

View File

@ -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"),

View File

@ -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"),
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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")
})
}
}

View File

@ -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"),
}

View File

@ -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))
}
}

View File

@ -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"),
}

View File

@ -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())
})
}
}

View File

@ -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"),
}

View File

@ -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)
}
})
}
}

View File

@ -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,
}
}

View File

@ -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())
})
}
}

View File

@ -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"),
}

View File

@ -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)
}
}

View File

@ -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"),
}

View File

@ -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())
})
}
}

View File

@ -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"),
}

View File

@ -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())
})
}
}

View File

@ -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 &copyrightClaimRepository{
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db),
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db, cfg),
db: db,
tracer: otel.Tracer("copyright_claim.repository"),
}

View File

@ -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 &copyrightRepository{
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db),
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db, cfg),
db: db,
tracer: otel.Tracer("copyright.repository"),
}

View File

@ -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)
})
}
}

View File

@ -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,
}
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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
}
}

View File

@ -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"),
}

View File

@ -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"),

View File

@ -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"),
}

View File

@ -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() {

View File

@ -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
}
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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),
}
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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"),
}

View File

@ -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() {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -76,6 +76,6 @@ func (s *SyncJob) SyncAllEntities(ctx context.Context) error {
// syncEntities is a generic function to sync a given entity type.
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)
}

View File

@ -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)
}
}

View File

@ -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,
}
}

View File

@ -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),

View File

@ -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
}

View File

@ -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()

View File

@ -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)
})
}
}

View File

@ -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")
}
}

View File

@ -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)