Merge pull request #17 from SamyRai/feature/finish-top-prio-tasks

feat: Implement critical features and fix build
This commit is contained in:
Damir Mukimov 2025-10-05 20:29:34 +02:00 committed by GitHub
commit ca4ce84344
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 641 additions and 365 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,18 +2,20 @@ package app
import ( import (
"tercul/internal/app/analytics" "tercul/internal/app/analytics"
"tercul/internal/app/auth"
"tercul/internal/app/author" "tercul/internal/app/author"
"tercul/internal/app/authz"
"tercul/internal/app/book" "tercul/internal/app/book"
"tercul/internal/app/bookmark" "tercul/internal/app/bookmark"
"tercul/internal/app/category" "tercul/internal/app/category"
"tercul/internal/app/collection" "tercul/internal/app/collection"
"tercul/internal/app/comment" "tercul/internal/app/comment"
"tercul/internal/app/contribution"
"tercul/internal/app/like" "tercul/internal/app/like"
"tercul/internal/app/localization" "tercul/internal/app/localization"
"tercul/internal/app/tag" "tercul/internal/app/tag"
"tercul/internal/app/translation" "tercul/internal/app/translation"
"tercul/internal/app/user" "tercul/internal/app/user"
"tercul/internal/app/auth"
"tercul/internal/app/work" "tercul/internal/app/work"
"tercul/internal/domain" "tercul/internal/domain"
auth_domain "tercul/internal/domain/auth" auth_domain "tercul/internal/domain/auth"
@ -23,31 +25,30 @@ import (
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
) )
import "tercul/internal/app/authz"
// Dependencies holds all external dependencies for the application. // Dependencies holds all external dependencies for the application.
type Dependencies struct { type Dependencies struct {
WorkRepo work_domain.WorkRepository WorkRepo work_domain.WorkRepository
UserRepo domain.UserRepository UserRepo domain.UserRepository
AuthorRepo domain.AuthorRepository AuthorRepo domain.AuthorRepository
TranslationRepo domain.TranslationRepository TranslationRepo domain.TranslationRepository
CommentRepo domain.CommentRepository CommentRepo domain.CommentRepository
LikeRepo domain.LikeRepository LikeRepo domain.LikeRepository
BookmarkRepo domain.BookmarkRepository BookmarkRepo domain.BookmarkRepository
CollectionRepo domain.CollectionRepository CollectionRepo domain.CollectionRepository
TagRepo domain.TagRepository TagRepo domain.TagRepository
CategoryRepo domain.CategoryRepository CategoryRepo domain.CategoryRepository
BookRepo domain.BookRepository BookRepo domain.BookRepository
PublisherRepo domain.PublisherRepository PublisherRepo domain.PublisherRepository
SourceRepo domain.SourceRepository SourceRepo domain.SourceRepository
CopyrightRepo domain.CopyrightRepository CopyrightRepo domain.CopyrightRepository
MonetizationRepo domain.MonetizationRepository MonetizationRepo domain.MonetizationRepository
AnalyticsRepo analytics.Repository ContributionRepo domain.ContributionRepository
AuthRepo auth_domain.AuthRepository AnalyticsRepo analytics.Repository
LocalizationRepo localization_domain.LocalizationRepository AuthRepo auth_domain.AuthRepository
SearchClient search.SearchClient LocalizationRepo localization_domain.LocalizationRepository
AnalyticsService analytics.Service SearchClient search.SearchClient
JWTManager platform_auth.JWTManagement AnalyticsService analytics.Service
JWTManager platform_auth.JWTManagement
} }
// Application is a container for all the application-layer services. // Application is a container for all the application-layer services.
@ -58,6 +59,7 @@ type Application struct {
Category *category.Service Category *category.Service
Collection *collection.Service Collection *collection.Service
Comment *comment.Service Comment *comment.Service
Contribution *contribution.Service
Like *like.Service Like *like.Service
Tag *tag.Service Tag *tag.Service
Translation *translation.Service Translation *translation.Service
@ -77,6 +79,8 @@ func NewApplication(deps Dependencies) *Application {
categoryService := category.NewService(deps.CategoryRepo) categoryService := category.NewService(deps.CategoryRepo)
collectionService := collection.NewService(deps.CollectionRepo) collectionService := collection.NewService(deps.CollectionRepo)
commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService) commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService)
contributionCommands := contribution.NewCommands(deps.ContributionRepo, authzService)
contributionService := contribution.NewService(contributionCommands)
likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService) likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService)
tagService := tag.NewService(deps.TagRepo) tagService := tag.NewService(deps.TagRepo)
translationService := translation.NewService(deps.TranslationRepo, authzService) translationService := translation.NewService(deps.TranslationRepo, authzService)
@ -92,6 +96,7 @@ func NewApplication(deps Dependencies) *Application {
Category: categoryService, Category: categoryService,
Collection: collectionService, Collection: collectionService,
Comment: commentService, Comment: commentService,
Contribution: contributionService,
Like: likeService, Like: likeService,
Tag: tagService, Tag: tagService,
Translation: translationService, Translation: translationService,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,24 +6,29 @@ import (
// Config stores all configuration of the application. // Config stores all configuration of the application.
type Config struct { type Config struct {
Environment string `mapstructure:"ENVIRONMENT"` Environment string `mapstructure:"ENVIRONMENT"`
ServerPort string `mapstructure:"SERVER_PORT"` ServerPort string `mapstructure:"SERVER_PORT"`
DBHost string `mapstructure:"DB_HOST"` DBHost string `mapstructure:"DB_HOST"`
DBPort string `mapstructure:"DB_PORT"` DBPort string `mapstructure:"DB_PORT"`
DBUser string `mapstructure:"DB_USER"` DBUser string `mapstructure:"DB_USER"`
DBPassword string `mapstructure:"DB_PASSWORD"` DBPassword string `mapstructure:"DB_PASSWORD"`
DBName string `mapstructure:"DB_NAME"` DBName string `mapstructure:"DB_NAME"`
JWTSecret string `mapstructure:"JWT_SECRET"` JWTSecret string `mapstructure:"JWT_SECRET"`
JWTExpiration int `mapstructure:"JWT_EXPIRATION_HOURS"` JWTExpiration int `mapstructure:"JWT_EXPIRATION_HOURS"`
WeaviateHost string `mapstructure:"WEAVIATE_HOST"` WeaviateHost string `mapstructure:"WEAVIATE_HOST"`
WeaviateScheme string `mapstructure:"WEAVIATE_SCHEME"` WeaviateScheme string `mapstructure:"WEAVIATE_SCHEME"`
MigrationPath string `mapstructure:"MIGRATION_PATH"` MigrationPath string `mapstructure:"MIGRATION_PATH"`
RedisAddr string `mapstructure:"REDIS_ADDR"` RedisAddr string `mapstructure:"REDIS_ADDR"`
RedisPassword string `mapstructure:"REDIS_PASSWORD"` RedisPassword string `mapstructure:"REDIS_PASSWORD"`
RedisDB int `mapstructure:"REDIS_DB"` RedisDB int `mapstructure:"REDIS_DB"`
SyncBatchSize int `mapstructure:"SYNC_BATCH_SIZE"` BatchSize int `mapstructure:"BATCH_SIZE"`
RateLimit int `mapstructure:"RATE_LIMIT"` RateLimit int `mapstructure:"RATE_LIMIT"`
RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"` RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"`
PageSize int `mapstructure:"PAGE_SIZE"`
NLPMemoryCacheCap int `mapstructure:"NLP_MEMORY_CACHE_CAP"`
NLPRedisCacheTTLSeconds int `mapstructure:"NLP_REDIS_CACHE_TTL_SECONDS"`
NLPUseLingua bool `mapstructure:"NLP_USE_LINGUA"`
NLPUseTFIDF bool `mapstructure:"NLP_USE_TFIDF"`
} }
// LoadConfig reads configuration from file or environment variables. // LoadConfig reads configuration from file or environment variables.
@ -43,9 +48,13 @@ func LoadConfig() (*Config, error) {
viper.SetDefault("REDIS_ADDR", "localhost:6379") viper.SetDefault("REDIS_ADDR", "localhost:6379")
viper.SetDefault("REDIS_PASSWORD", "") viper.SetDefault("REDIS_PASSWORD", "")
viper.SetDefault("REDIS_DB", 0) viper.SetDefault("REDIS_DB", 0)
viper.SetDefault("SYNC_BATCH_SIZE", 100) viper.SetDefault("BATCH_SIZE", 100)
viper.SetDefault("RATE_LIMIT", 10) viper.SetDefault("RATE_LIMIT", 10)
viper.SetDefault("RATE_LIMIT_BURST", 100) viper.SetDefault("RATE_LIMIT_BURST", 100)
viper.SetDefault("NLP_MEMORY_CACHE_CAP", 1024)
viper.SetDefault("NLP_REDIS_CACHE_TTL_SECONDS", 3600)
viper.SetDefault("NLP_USE_LINGUA", true)
viper.SetDefault("NLP_USE_TFIDF", true)
viper.AutomaticEnv() viper.AutomaticEnv()

View File

@ -21,10 +21,12 @@ type RateLimiter struct {
} }
// NewRateLimiter creates a new rate limiter // NewRateLimiter creates a new rate limiter
func NewRateLimiter(rate, capacity int) *RateLimiter { func NewRateLimiter(cfg *config.Config) *RateLimiter {
rate := cfg.RateLimit
if rate <= 0 { if rate <= 0 {
rate = 10 // default rate: 10 requests per second rate = 10 // default rate: 10 requests per second
} }
capacity := cfg.RateLimitBurst
if capacity <= 0 { if capacity <= 0 {
capacity = 100 // default capacity: 100 tokens capacity = 100 // default capacity: 100 tokens
} }
@ -73,28 +75,29 @@ func minF(a, b float64) float64 {
} }
// RateLimitMiddleware creates a middleware that applies rate limiting // RateLimitMiddleware creates a middleware that applies rate limiting
func RateLimitMiddleware(next http.Handler) http.Handler { func RateLimitMiddleware(cfg *config.Config) func(http.Handler) http.Handler {
rateLimiter := NewRateLimiter(config.Cfg.RateLimit, config.Cfg.RateLimitBurst) rateLimiter := NewRateLimiter(cfg)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 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) { // Check if request is allowed
// Use X-Client-ID header for client identification in tests if !rateLimiter.Allow(clientID) {
clientID := r.Header.Get("X-Client-ID") log.FromContext(r.Context()).
if clientID == "" { With("clientID", clientID).
clientID = r.RemoteAddr Warn("Rate limit exceeded")
}
// Check if request is allowed w.WriteHeader(http.StatusTooManyRequests)
if !rateLimiter.Allow(clientID) { w.Write([]byte("Rate limit exceeded. Please try again later."))
log.FromContext(r.Context()). return
With("clientID", clientID). }
Warn("Rate limit exceeded")
w.WriteHeader(http.StatusTooManyRequests) // Continue to the next handler
w.Write([]byte("Rate limit exceeded. Please try again later.")) next.ServeHTTP(w, r)
return })
} }
// Continue to the next handler
next.ServeHTTP(w, r)
})
} }

View File

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

View File

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