mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 00:31:35 +00:00
feat: Implement critical features and fix build
This commit addresses several high-priority tasks from the TASKS.md file, including: - **Fix Background Job Panic:** Replaced `log.Fatalf` with `log.Printf` in the `asynq` server to prevent crashes. - **Refactor API Server Setup:** Consolidated the GraphQL Playground and Prometheus metrics endpoints into the main API server. - **Implement `DeleteUser` Mutation:** Implemented the `DeleteUser` resolver. - **Implement `CreateContribution` Mutation:** Implemented the `CreateContribution` resolver and its required application service. Additionally, this commit includes a major refactoring of the configuration management system to fix a broken build. The global `config.Cfg` variable has been removed and replaced with a dependency injection approach, where the configuration object is passed to all components that require it. This change has been applied across the entire codebase, including the test suite, to ensure a stable and testable application.
This commit is contained in:
parent
37a007b08c
commit
a8dfb727a1
124
cmd/api/main.go
124
cmd/api/main.go
@ -3,11 +3,10 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"tercul/internal/app"
|
||||
"tercul/internal/app/analytics"
|
||||
@ -18,7 +17,7 @@ import (
|
||||
"tercul/internal/platform/auth"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/platform/db"
|
||||
"tercul/internal/platform/log"
|
||||
app_log "tercul/internal/platform/log"
|
||||
"tercul/internal/platform/search"
|
||||
"time"
|
||||
|
||||
@ -30,7 +29,7 @@ import (
|
||||
)
|
||||
|
||||
// runMigrations applies database migrations using goose.
|
||||
func runMigrations(gormDB *gorm.DB) error {
|
||||
func runMigrations(gormDB *gorm.DB, migrationPath string) error {
|
||||
sqlDB, err := gormDB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -40,35 +39,34 @@ func runMigrations(gormDB *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// This is brittle. A better approach might be to use an env var or config.
|
||||
_, b, _, _ := runtime.Caller(0)
|
||||
migrationsDir := filepath.Join(filepath.Dir(b), "../../internal/data/migrations")
|
||||
|
||||
log.Info(fmt.Sprintf("Applying database migrations from %s", migrationsDir))
|
||||
if err := goose.Up(sqlDB, migrationsDir); err != nil {
|
||||
app_log.Info(fmt.Sprintf("Applying database migrations from %s", migrationPath))
|
||||
if err := goose.Up(sqlDB, migrationPath); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Database migrations applied successfully")
|
||||
app_log.Info("Database migrations applied successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// main is the entry point for the Tercul application.
|
||||
func main() {
|
||||
// Load configuration from environment variables
|
||||
config.LoadConfig()
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("cannot load config: %v", err)
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
log.Init("tercul-api", config.Cfg.Environment)
|
||||
obsLogger := observability.NewLogger("tercul-api", config.Cfg.Environment)
|
||||
app_log.Init("tercul-api", cfg.Environment)
|
||||
obsLogger := observability.NewLogger("tercul-api", cfg.Environment)
|
||||
|
||||
// Initialize OpenTelemetry Tracer Provider
|
||||
tp, err := observability.TracerProvider("tercul-api", config.Cfg.Environment)
|
||||
tp, err := observability.TracerProvider("tercul-api", cfg.Environment)
|
||||
if err != nil {
|
||||
log.Fatal(err, "Failed to initialize OpenTelemetry tracer")
|
||||
app_log.Fatal(err, "Failed to initialize OpenTelemetry tracer")
|
||||
}
|
||||
defer func() {
|
||||
if err := tp.Shutdown(context.Background()); err != nil {
|
||||
log.Error(err, "Error shutting down tracer provider")
|
||||
app_log.Error(err, "Error shutting down tracer provider")
|
||||
}
|
||||
}()
|
||||
|
||||
@ -76,71 +74,72 @@ func main() {
|
||||
reg := prometheus.NewRegistry()
|
||||
metrics := observability.NewMetrics(reg) // Metrics are registered automatically
|
||||
|
||||
log.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", config.Cfg.Environment))
|
||||
app_log.Info(fmt.Sprintf("Starting Tercul application in %s environment, version 1.0.0", cfg.Environment))
|
||||
|
||||
// Initialize database connection
|
||||
database, err := db.InitDB(metrics)
|
||||
database, err := db.InitDB(cfg, metrics)
|
||||
if err != nil {
|
||||
log.Fatal(err, "Failed to initialize database")
|
||||
app_log.Fatal(err, "Failed to initialize database")
|
||||
}
|
||||
defer db.Close()
|
||||
defer db.Close(database)
|
||||
|
||||
if err := runMigrations(database); err != nil {
|
||||
log.Fatal(err, "Failed to apply database migrations")
|
||||
if err := runMigrations(database, cfg.MigrationPath); err != nil {
|
||||
app_log.Fatal(err, "Failed to apply database migrations")
|
||||
}
|
||||
|
||||
// Initialize Weaviate client
|
||||
weaviateCfg := weaviate.Config{
|
||||
Host: config.Cfg.WeaviateHost,
|
||||
Scheme: config.Cfg.WeaviateScheme,
|
||||
Host: cfg.WeaviateHost,
|
||||
Scheme: cfg.WeaviateScheme,
|
||||
}
|
||||
weaviateClient, err := weaviate.NewClient(weaviateCfg)
|
||||
if err != nil {
|
||||
log.Fatal(err, "Failed to create weaviate client")
|
||||
app_log.Fatal(err, "Failed to create weaviate client")
|
||||
}
|
||||
|
||||
// Create search client
|
||||
searchClient := search.NewWeaviateWrapper(weaviateClient)
|
||||
|
||||
// Create repositories
|
||||
repos := dbsql.NewRepositories(database)
|
||||
repos := dbsql.NewRepositories(database, cfg)
|
||||
|
||||
// Create linguistics dependencies
|
||||
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
|
||||
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
||||
if err != nil {
|
||||
log.Fatal(err, "Failed to create sentiment provider")
|
||||
app_log.Fatal(err, "Failed to create sentiment provider")
|
||||
}
|
||||
|
||||
// Create platform components
|
||||
jwtManager := auth.NewJWTManager()
|
||||
jwtManager := auth.NewJWTManager(cfg)
|
||||
|
||||
// Create application services
|
||||
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
|
||||
|
||||
// Create application dependencies
|
||||
deps := app.Dependencies{
|
||||
WorkRepo: repos.Work,
|
||||
UserRepo: repos.User,
|
||||
AuthorRepo: repos.Author,
|
||||
TranslationRepo: repos.Translation,
|
||||
CommentRepo: repos.Comment,
|
||||
LikeRepo: repos.Like,
|
||||
BookmarkRepo: repos.Bookmark,
|
||||
CollectionRepo: repos.Collection,
|
||||
TagRepo: repos.Tag,
|
||||
CategoryRepo: repos.Category,
|
||||
BookRepo: repos.Book,
|
||||
PublisherRepo: repos.Publisher,
|
||||
SourceRepo: repos.Source,
|
||||
CopyrightRepo: repos.Copyright,
|
||||
MonetizationRepo: repos.Monetization,
|
||||
AnalyticsRepo: repos.Analytics,
|
||||
AuthRepo: repos.Auth,
|
||||
LocalizationRepo: repos.Localization,
|
||||
SearchClient: searchClient,
|
||||
AnalyticsService: analyticsService,
|
||||
JWTManager: jwtManager,
|
||||
WorkRepo: repos.Work,
|
||||
UserRepo: repos.User,
|
||||
AuthorRepo: repos.Author,
|
||||
TranslationRepo: repos.Translation,
|
||||
CommentRepo: repos.Comment,
|
||||
LikeRepo: repos.Like,
|
||||
BookmarkRepo: repos.Bookmark,
|
||||
CollectionRepo: repos.Collection,
|
||||
TagRepo: repos.Tag,
|
||||
CategoryRepo: repos.Category,
|
||||
BookRepo: repos.Book,
|
||||
PublisherRepo: repos.Publisher,
|
||||
SourceRepo: repos.Source,
|
||||
CopyrightRepo: repos.Copyright,
|
||||
MonetizationRepo: repos.Monetization,
|
||||
ContributionRepo: repos.Contribution,
|
||||
AnalyticsRepo: repos.Analytics,
|
||||
AuthRepo: repos.Auth,
|
||||
LocalizationRepo: repos.Localization,
|
||||
SearchClient: searchClient,
|
||||
AnalyticsService: analyticsService,
|
||||
JWTManager: jwtManager,
|
||||
}
|
||||
|
||||
// Create application
|
||||
@ -151,28 +150,27 @@ func main() {
|
||||
App: application,
|
||||
}
|
||||
|
||||
// Create handlers
|
||||
// Create the main API handler with all middleware.
|
||||
// NewServerWithAuth now returns the handler chain directly.
|
||||
apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
|
||||
playgroundHandler := playground.Handler("GraphQL Playground", "/query")
|
||||
metricsHandler := observability.PrometheusHandler(reg)
|
||||
|
||||
// Consolidate handlers into a single mux
|
||||
// Create the main ServeMux and register all handlers.
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/query", apiHandler)
|
||||
mux.Handle("/playground", playgroundHandler)
|
||||
mux.Handle("/metrics", metricsHandler)
|
||||
mux.Handle("/playground", playground.Handler("GraphQL Playground", "/query"))
|
||||
mux.Handle("/metrics", observability.PrometheusHandler(reg))
|
||||
|
||||
// Create a single HTTP server
|
||||
// Create a single HTTP server with the main mux.
|
||||
mainServer := &http.Server{
|
||||
Addr: config.Cfg.ServerPort,
|
||||
Addr: cfg.ServerPort,
|
||||
Handler: mux,
|
||||
}
|
||||
log.Info(fmt.Sprintf("API server listening on port %s", config.Cfg.ServerPort))
|
||||
app_log.Info(fmt.Sprintf("API server listening on port %s", cfg.ServerPort))
|
||||
|
||||
// Start the main server in a goroutine
|
||||
go func() {
|
||||
if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err, "Failed to start server")
|
||||
app_log.Fatal(err, "Failed to start server")
|
||||
}
|
||||
}()
|
||||
|
||||
@ -180,15 +178,15 @@ func main() {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Info("Shutting down server...")
|
||||
app_log.Info("Shutting down server...")
|
||||
|
||||
// Graceful shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := mainServer.Shutdown(ctx); err != nil {
|
||||
log.Error(err, "Server forced to shutdown")
|
||||
app_log.Error(err, "Server forced to shutdown")
|
||||
}
|
||||
|
||||
log.Info("Server shut down successfully")
|
||||
app_log.Info("Server shut down successfully")
|
||||
}
|
||||
@ -9,19 +9,6 @@ import (
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
)
|
||||
|
||||
// NewServer creates a new GraphQL server with the given resolver
|
||||
func NewServer(resolver *graphql.Resolver) http.Handler {
|
||||
c := graphql.Config{Resolvers: resolver}
|
||||
c.Directives.Binding = graphql.Binding
|
||||
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
|
||||
|
||||
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/query", srv)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// NewServerWithAuth creates a new GraphQL server with authentication and observability middleware
|
||||
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager, metrics *observability.Metrics, logger *observability.Logger) http.Handler {
|
||||
c := graphql.Config{Resolvers: resolver}
|
||||
@ -42,9 +29,6 @@ func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager,
|
||||
chain = observability.TracingMiddleware(chain)
|
||||
chain = observability.RequestIDMiddleware(chain)
|
||||
|
||||
// Create a mux to handle GraphQL endpoint
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/query", chain)
|
||||
|
||||
return mux
|
||||
// Return the handler chain directly. The caller is responsible for routing.
|
||||
return chain
|
||||
}
|
||||
@ -31,15 +31,18 @@ func main() {
|
||||
}
|
||||
|
||||
// 2. Initialize dependencies
|
||||
config.LoadConfig()
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err, "Failed to load config")
|
||||
}
|
||||
log.Init("enrich-tool", "development")
|
||||
database, err := db.InitDB(nil) // No metrics needed for this tool
|
||||
database, err := db.InitDB(cfg, nil) // No metrics needed for this tool
|
||||
if err != nil {
|
||||
log.Fatal(err, "Failed to initialize database")
|
||||
}
|
||||
defer db.Close()
|
||||
defer db.Close(database)
|
||||
|
||||
repos := sql.NewRepositories(database)
|
||||
repos := sql.NewRepositories(database, cfg)
|
||||
enrichmentSvc := enrichment.NewService()
|
||||
|
||||
// 3. Fetch, enrich, and save the entity
|
||||
|
||||
@ -20,6 +20,7 @@ import (
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/observability"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
platform_config "tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
@ -58,7 +59,9 @@ func (s *GraphQLIntegrationSuite) CreateAuthenticatedUser(username, email string
|
||||
user.Role = role
|
||||
|
||||
// Re-generate token with the new role
|
||||
jwtManager := platform_auth.NewJWTManager()
|
||||
cfg, err := platform_config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||
newToken, err := jwtManager.GenerateToken(user)
|
||||
s.Require().NoError(err)
|
||||
token = newToken
|
||||
@ -81,7 +84,9 @@ func (s *GraphQLIntegrationSuite) SetupSuite() {
|
||||
srv.SetErrorPresenter(graph.NewErrorPresenter())
|
||||
|
||||
// Create JWT manager and middleware
|
||||
jwtManager := platform_auth.NewJWTManager()
|
||||
cfg, err := platform_config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||
reg := prometheus.NewRegistry()
|
||||
metrics := observability.NewMetrics(reg)
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"tercul/internal/app/bookmark"
|
||||
"tercul/internal/app/collection"
|
||||
"tercul/internal/app/comment"
|
||||
"tercul/internal/app/contribution"
|
||||
"tercul/internal/app/like"
|
||||
"tercul/internal/app/translation"
|
||||
"tercul/internal/app/user"
|
||||
@ -502,7 +503,17 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
||||
|
||||
// DeleteUser is the resolver for the deleteUser field.
|
||||
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
|
||||
panic(fmt.Errorf("not implemented: DeleteUser - deleteUser"))
|
||||
userID, err := strconv.ParseUint(id, 10, 32)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
err = r.App.User.Commands.DeleteUser(ctx, uint(userID))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CreateCollection is the resolver for the createCollection field.
|
||||
@ -1020,7 +1031,56 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
|
||||
|
||||
// CreateContribution is the resolver for the createContribution field.
|
||||
func (r *mutationResolver) CreateContribution(ctx context.Context, input model.ContributionInput) (*model.Contribution, error) {
|
||||
panic(fmt.Errorf("not implemented: CreateContribution - createContribution"))
|
||||
// Get user ID from context
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
// Convert GraphQL input to service input
|
||||
createInput := contribution.CreateContributionInput{
|
||||
Name: input.Name,
|
||||
}
|
||||
|
||||
if input.WorkID != nil {
|
||||
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
wID := uint(workID)
|
||||
createInput.WorkID = &wID
|
||||
}
|
||||
|
||||
if input.TranslationID != nil {
|
||||
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||
}
|
||||
tID := uint(translationID)
|
||||
createInput.TranslationID = &tID
|
||||
}
|
||||
|
||||
if input.Status != nil {
|
||||
createInput.Status = input.Status.String()
|
||||
} else {
|
||||
createInput.Status = "DRAFT" // Default status
|
||||
}
|
||||
|
||||
// Call contribution service
|
||||
createdContribution, err := r.App.Contribution.Commands.CreateContribution(ctx, createInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to GraphQL model
|
||||
return &model.Contribution{
|
||||
ID: fmt.Sprintf("%d", createdContribution.ID),
|
||||
Name: createdContribution.Name,
|
||||
Status: model.ContributionStatus(createdContribution.Status),
|
||||
User: &model.User{
|
||||
ID: fmt.Sprintf("%d", userID),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateContribution is the resolver for the updateContribution field.
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -21,10 +22,12 @@ type AnalyticsServiceTestSuite struct {
|
||||
|
||||
func (s *AnalyticsServiceTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
analyticsRepo := sql.NewAnalyticsRepository(s.DB)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
analyticsRepo := sql.NewAnalyticsRepository(s.DB, cfg)
|
||||
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
|
||||
translationRepo := sql.NewTranslationRepository(s.DB)
|
||||
workRepo := sql.NewWorkRepository(s.DB)
|
||||
translationRepo := sql.NewTranslationRepository(s.DB, cfg)
|
||||
workRepo := sql.NewWorkRepository(s.DB, cfg)
|
||||
sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider()
|
||||
s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, workRepo, sentimentProvider)
|
||||
}
|
||||
|
||||
@ -2,18 +2,20 @@ package app
|
||||
|
||||
import (
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/app/author"
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/app/book"
|
||||
"tercul/internal/app/bookmark"
|
||||
"tercul/internal/app/category"
|
||||
"tercul/internal/app/collection"
|
||||
"tercul/internal/app/comment"
|
||||
"tercul/internal/app/contribution"
|
||||
"tercul/internal/app/like"
|
||||
"tercul/internal/app/localization"
|
||||
"tercul/internal/app/tag"
|
||||
"tercul/internal/app/translation"
|
||||
"tercul/internal/app/user"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/app/work"
|
||||
"tercul/internal/domain"
|
||||
auth_domain "tercul/internal/domain/auth"
|
||||
@ -23,31 +25,30 @@ import (
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
)
|
||||
|
||||
import "tercul/internal/app/authz"
|
||||
|
||||
// Dependencies holds all external dependencies for the application.
|
||||
type Dependencies struct {
|
||||
WorkRepo work_domain.WorkRepository
|
||||
UserRepo domain.UserRepository
|
||||
AuthorRepo domain.AuthorRepository
|
||||
TranslationRepo domain.TranslationRepository
|
||||
CommentRepo domain.CommentRepository
|
||||
LikeRepo domain.LikeRepository
|
||||
BookmarkRepo domain.BookmarkRepository
|
||||
CollectionRepo domain.CollectionRepository
|
||||
TagRepo domain.TagRepository
|
||||
CategoryRepo domain.CategoryRepository
|
||||
BookRepo domain.BookRepository
|
||||
PublisherRepo domain.PublisherRepository
|
||||
SourceRepo domain.SourceRepository
|
||||
CopyrightRepo domain.CopyrightRepository
|
||||
MonetizationRepo domain.MonetizationRepository
|
||||
AnalyticsRepo analytics.Repository
|
||||
AuthRepo auth_domain.AuthRepository
|
||||
LocalizationRepo localization_domain.LocalizationRepository
|
||||
SearchClient search.SearchClient
|
||||
AnalyticsService analytics.Service
|
||||
JWTManager platform_auth.JWTManagement
|
||||
WorkRepo work_domain.WorkRepository
|
||||
UserRepo domain.UserRepository
|
||||
AuthorRepo domain.AuthorRepository
|
||||
TranslationRepo domain.TranslationRepository
|
||||
CommentRepo domain.CommentRepository
|
||||
LikeRepo domain.LikeRepository
|
||||
BookmarkRepo domain.BookmarkRepository
|
||||
CollectionRepo domain.CollectionRepository
|
||||
TagRepo domain.TagRepository
|
||||
CategoryRepo domain.CategoryRepository
|
||||
BookRepo domain.BookRepository
|
||||
PublisherRepo domain.PublisherRepository
|
||||
SourceRepo domain.SourceRepository
|
||||
CopyrightRepo domain.CopyrightRepository
|
||||
MonetizationRepo domain.MonetizationRepository
|
||||
ContributionRepo domain.ContributionRepository
|
||||
AnalyticsRepo analytics.Repository
|
||||
AuthRepo auth_domain.AuthRepository
|
||||
LocalizationRepo localization_domain.LocalizationRepository
|
||||
SearchClient search.SearchClient
|
||||
AnalyticsService analytics.Service
|
||||
JWTManager platform_auth.JWTManagement
|
||||
}
|
||||
|
||||
// Application is a container for all the application-layer services.
|
||||
@ -58,6 +59,7 @@ type Application struct {
|
||||
Category *category.Service
|
||||
Collection *collection.Service
|
||||
Comment *comment.Service
|
||||
Contribution *contribution.Service
|
||||
Like *like.Service
|
||||
Tag *tag.Service
|
||||
Translation *translation.Service
|
||||
@ -77,6 +79,8 @@ func NewApplication(deps Dependencies) *Application {
|
||||
categoryService := category.NewService(deps.CategoryRepo)
|
||||
collectionService := collection.NewService(deps.CollectionRepo)
|
||||
commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService)
|
||||
contributionCommands := contribution.NewCommands(deps.ContributionRepo, authzService)
|
||||
contributionService := contribution.NewService(contributionCommands)
|
||||
likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService)
|
||||
tagService := tag.NewService(deps.TagRepo)
|
||||
translationService := translation.NewService(deps.TranslationRepo, authzService)
|
||||
@ -92,6 +96,7 @@ func NewApplication(deps Dependencies) *Application {
|
||||
Category: categoryService,
|
||||
Collection: collectionService,
|
||||
Comment: commentService,
|
||||
Contribution: contributionService,
|
||||
Like: likeService,
|
||||
Tag: tagService,
|
||||
Translation: translationService,
|
||||
|
||||
55
internal/app/contribution/commands.go
Normal file
55
internal/app/contribution/commands.go
Normal 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
|
||||
}
|
||||
14
internal/app/contribution/service.go
Normal file
14
internal/app/contribution/service.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -172,7 +173,9 @@ func TestMergeWork_Integration(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create real repositories and services pointing to the test DB
|
||||
workRepo := sql.NewWorkRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
assert.NoError(t, err)
|
||||
workRepo := sql.NewWorkRepository(db, cfg)
|
||||
authzSvc := authz.NewService(workRepo, nil) // Using real repo for authz checks
|
||||
searchClient := &mockSearchClient{} // Mock search client is fine
|
||||
commands := NewWorkCommands(workRepo, searchClient, authzSvc)
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
@ -18,7 +19,7 @@ type analyticsRepository struct {
|
||||
tracer trace.Tracer
|
||||
}
|
||||
|
||||
func NewAnalyticsRepository(db *gorm.DB) analytics.Repository {
|
||||
func NewAnalyticsRepository(db *gorm.DB, cfg *config.Config) analytics.Repository {
|
||||
return &analyticsRepository{
|
||||
db: db,
|
||||
tracer: otel.Tracer("analytics.repository"),
|
||||
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain/auth"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
@ -15,7 +16,7 @@ type authRepository struct {
|
||||
tracer trace.Tracer
|
||||
}
|
||||
|
||||
func NewAuthRepository(db *gorm.DB) auth.AuthRepository {
|
||||
func NewAuthRepository(db *gorm.DB, cfg *config.Config) auth.AuthRepository {
|
||||
return &authRepository{
|
||||
db: db,
|
||||
tracer: otel.Tracer("auth.repository"),
|
||||
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type authorRepository struct {
|
||||
}
|
||||
|
||||
// NewAuthorRepository creates a new AuthorRepository.
|
||||
func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository {
|
||||
func NewAuthorRepository(db *gorm.DB, cfg *config.Config) domain.AuthorRepository {
|
||||
return &authorRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Author](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Author](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("author.repository"),
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -17,7 +18,9 @@ type AuthorRepositoryTestSuite struct {
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
s.AuthorRepo = sql.NewAuthorRepository(s.DB)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.AuthorRepo = sql.NewAuthorRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) SetupTest() {
|
||||
@ -58,4 +61,4 @@ func (s *AuthorRepositoryTestSuite) TestListByWorkID() {
|
||||
|
||||
func TestAuthorRepository(t *testing.T) {
|
||||
suite.Run(t, new(AuthorRepositoryTestSuite))
|
||||
}
|
||||
}
|
||||
@ -28,13 +28,15 @@ var (
|
||||
type BaseRepositoryImpl[T any] struct {
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewBaseRepositoryImpl creates a new BaseRepositoryImpl
|
||||
func NewBaseRepositoryImpl[T any](db *gorm.DB) domain.BaseRepository[T] {
|
||||
func NewBaseRepositoryImpl[T any](db *gorm.DB, cfg *config.Config) domain.BaseRepository[T] {
|
||||
return &BaseRepositoryImpl[T]{
|
||||
db: db,
|
||||
tracer: otel.Tracer("base.repository"),
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +71,7 @@ func (r *BaseRepositoryImpl[T]) validatePagination(page, pageSize int) (int, int
|
||||
}
|
||||
|
||||
if pageSize < 1 {
|
||||
pageSize = config.Cfg.PageSize
|
||||
pageSize = r.cfg.PageSize
|
||||
if pageSize < 1 {
|
||||
pageSize = 20 // Default page size
|
||||
}
|
||||
@ -525,7 +527,7 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
|
||||
defer span.End()
|
||||
|
||||
if batchSize <= 0 {
|
||||
batchSize = config.Cfg.BatchSize
|
||||
batchSize = r.cfg.BatchSize
|
||||
if batchSize <= 0 {
|
||||
batchSize = 100 // Default batch size
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -16,12 +17,16 @@ import (
|
||||
type BaseRepositoryTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
repo domain.BaseRepository[testutil.TestEntity]
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// SetupSuite initializes the test suite, database, and repository.
|
||||
func (s *BaseRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.cfg = cfg
|
||||
s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB, s.cfg)
|
||||
}
|
||||
|
||||
// SetupTest cleans the database before each test.
|
||||
@ -219,7 +224,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
|
||||
// Act
|
||||
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
|
||||
entity := &testutil.TestEntity{Name: "TX Commit"}
|
||||
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx)
|
||||
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
|
||||
if err := repoInTx.Create(context.Background(), entity); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -241,7 +246,7 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
|
||||
// Act
|
||||
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
|
||||
entity := &testutil.TestEntity{Name: "TX Rollback"}
|
||||
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx)
|
||||
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg)
|
||||
if err := repoInTx.Create(context.Background(), entity); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -256,4 +261,4 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
|
||||
_, getErr := s.repo.GetByID(context.Background(), createdID)
|
||||
s.ErrorIs(getErr, sql.ErrEntityNotFound, "Entity should not exist after rollback")
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type bookRepository struct {
|
||||
}
|
||||
|
||||
// NewBookRepository creates a new BookRepository.
|
||||
func NewBookRepository(db *gorm.DB) domain.BookRepository {
|
||||
func NewBookRepository(db *gorm.DB, cfg *config.Config) domain.BookRepository {
|
||||
return &bookRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Book](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Book](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("book.repository"),
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -17,7 +18,9 @@ type BookRepositoryTestSuite struct {
|
||||
|
||||
func (s *BookRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
s.BookRepo = sql.NewBookRepository(s.DB)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.BookRepo = sql.NewBookRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) SetupTest() {
|
||||
@ -65,4 +68,4 @@ func (s *BookRepositoryTestSuite) TestFindByISBN() {
|
||||
|
||||
func TestBookRepository(t *testing.T) {
|
||||
suite.Run(t, new(BookRepositoryTestSuite))
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type bookmarkRepository struct {
|
||||
}
|
||||
|
||||
// NewBookmarkRepository creates a new BookmarkRepository.
|
||||
func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository {
|
||||
func NewBookmarkRepository(db *gorm.DB, cfg *config.Config) domain.BookmarkRepository {
|
||||
return &bookmarkRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("bookmark.repository"),
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
repo "tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
@ -17,7 +18,9 @@ import (
|
||||
func TestNewBookmarkRepository(t *testing.T) {
|
||||
db, _, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db, cfg)
|
||||
assert.NotNil(t, repo)
|
||||
}
|
||||
|
||||
@ -25,7 +28,9 @@ func TestBookmarkRepository_ListByUserID(t *testing.T) {
|
||||
t.Run("should return bookmarks for a given user id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db, cfg)
|
||||
|
||||
userID := uint(1)
|
||||
expectedBookmarks := []domain.Bookmark{
|
||||
@ -50,7 +55,9 @@ func TestBookmarkRepository_ListByUserID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db, cfg)
|
||||
|
||||
userID := uint(1)
|
||||
|
||||
@ -69,7 +76,9 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) {
|
||||
t.Run("should return bookmarks for a given work id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db, cfg)
|
||||
|
||||
workID := uint(1)
|
||||
expectedBookmarks := []domain.Bookmark{
|
||||
@ -94,7 +103,9 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewBookmarkRepository(db, cfg)
|
||||
|
||||
workID := uint(1)
|
||||
|
||||
@ -107,4 +118,4 @@ func TestBookmarkRepository_ListByWorkID(t *testing.T) {
|
||||
assert.Nil(t, bookmarks)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type categoryRepository struct {
|
||||
}
|
||||
|
||||
// NewCategoryRepository creates a new CategoryRepository.
|
||||
func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository {
|
||||
func NewCategoryRepository(db *gorm.DB, cfg *config.Config) domain.CategoryRepository {
|
||||
return &categoryRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Category](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Category](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("category.repository"),
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -17,7 +18,9 @@ type CategoryRepositoryTestSuite struct {
|
||||
|
||||
func (s *CategoryRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
s.CategoryRepo = sql.NewCategoryRepository(s.DB)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.CategoryRepo = sql.NewCategoryRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *CategoryRepositoryTestSuite) SetupTest() {
|
||||
@ -111,4 +114,4 @@ func (s *CategoryRepositoryTestSuite) TestListByParentID() {
|
||||
s.Equal(parent.ID, *cat.ParentID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -13,9 +14,9 @@ type cityRepository struct {
|
||||
}
|
||||
|
||||
// NewCityRepository creates a new CityRepository.
|
||||
func NewCityRepository(db *gorm.DB) domain.CityRepository {
|
||||
func NewCityRepository(db *gorm.DB, cfg *config.Config) domain.CityRepository {
|
||||
return &cityRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.City](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.City](db, cfg),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
repo "tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
@ -17,7 +18,9 @@ import (
|
||||
func TestNewCityRepository(t *testing.T) {
|
||||
db, _, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCityRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCityRepository(db, cfg)
|
||||
assert.NotNil(t, repo)
|
||||
}
|
||||
|
||||
@ -25,7 +28,9 @@ func TestCityRepository_ListByCountryID(t *testing.T) {
|
||||
t.Run("should return cities for a given country id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCityRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCityRepository(db, cfg)
|
||||
|
||||
countryID := uint(1)
|
||||
expectedCities := []domain.City{
|
||||
@ -50,7 +55,9 @@ func TestCityRepository_ListByCountryID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCityRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCityRepository(db, cfg)
|
||||
|
||||
countryID := uint(1)
|
||||
|
||||
@ -63,4 +70,4 @@ func TestCityRepository_ListByCountryID(t *testing.T) {
|
||||
assert.Nil(t, cities)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type collectionRepository struct {
|
||||
}
|
||||
|
||||
// NewCollectionRepository creates a new CollectionRepository.
|
||||
func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository {
|
||||
func NewCollectionRepository(db *gorm.DB, cfg *config.Config) domain.CollectionRepository {
|
||||
return &collectionRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("collection.repository"),
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -28,7 +29,9 @@ func (s *CollectionRepositoryTestSuite) SetupTest() {
|
||||
|
||||
s.db = gormDB
|
||||
s.mock = mock
|
||||
s.repo = sql.NewCollectionRepository(s.db)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.repo = sql.NewCollectionRepository(s.db, cfg)
|
||||
}
|
||||
|
||||
func (s *CollectionRepositoryTestSuite) TearDownTest() {
|
||||
@ -101,4 +104,4 @@ func (s *CollectionRepositoryTestSuite) TestListByWorkID() {
|
||||
collections, err := s.repo.ListByWorkID(context.Background(), workID)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(collections, 2)
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type commentRepository struct {
|
||||
}
|
||||
|
||||
// NewCommentRepository creates a new CommentRepository.
|
||||
func NewCommentRepository(db *gorm.DB) domain.CommentRepository {
|
||||
func NewCommentRepository(db *gorm.DB, cfg *config.Config) domain.CommentRepository {
|
||||
return &commentRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("comment.repository"),
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
repo "tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
@ -17,7 +18,9 @@ import (
|
||||
func TestNewCommentRepository(t *testing.T) {
|
||||
db, _, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
assert.NotNil(t, repo)
|
||||
}
|
||||
|
||||
@ -25,7 +28,9 @@ func TestCommentRepository_ListByUserID(t *testing.T) {
|
||||
t.Run("should return comments for a given user id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
userID := uint(1)
|
||||
expectedComments := []domain.Comment{
|
||||
@ -50,7 +55,9 @@ func TestCommentRepository_ListByUserID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
userID := uint(1)
|
||||
|
||||
@ -69,7 +76,9 @@ func TestCommentRepository_ListByWorkID(t *testing.T) {
|
||||
t.Run("should return comments for a given work id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
workID := uint(1)
|
||||
expectedComments := []domain.Comment{
|
||||
@ -94,7 +103,9 @@ func TestCommentRepository_ListByWorkID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
workID := uint(1)
|
||||
|
||||
@ -113,7 +124,9 @@ func TestCommentRepository_ListByTranslationID(t *testing.T) {
|
||||
t.Run("should return comments for a given translation id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
translationID := uint(1)
|
||||
expectedComments := []domain.Comment{
|
||||
@ -138,7 +151,9 @@ func TestCommentRepository_ListByTranslationID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
translationID := uint(1)
|
||||
|
||||
@ -157,7 +172,9 @@ func TestCommentRepository_ListByParentID(t *testing.T) {
|
||||
t.Run("should return comments for a given parent id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
parentID := uint(1)
|
||||
expectedComments := []domain.Comment{
|
||||
@ -182,7 +199,9 @@ func TestCommentRepository_ListByParentID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewCommentRepository(db, cfg)
|
||||
|
||||
parentID := uint(1)
|
||||
|
||||
@ -195,4 +214,4 @@ func TestCommentRepository_ListByParentID(t *testing.T) {
|
||||
assert.Nil(t, comments)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type contributionRepository struct {
|
||||
}
|
||||
|
||||
// NewContributionRepository creates a new ContributionRepository.
|
||||
func NewContributionRepository(db *gorm.DB) domain.ContributionRepository {
|
||||
func NewContributionRepository(db *gorm.DB, cfg *config.Config) domain.ContributionRepository {
|
||||
return &contributionRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("contribution.repository"),
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
repo "tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
@ -17,7 +18,9 @@ import (
|
||||
func TestNewContributionRepository(t *testing.T) {
|
||||
db, _, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
assert.NotNil(t, repo)
|
||||
}
|
||||
|
||||
@ -25,7 +28,9 @@ func TestContributionRepository_ListByUserID(t *testing.T) {
|
||||
t.Run("should return contributions for a given user id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
userID := uint(1)
|
||||
expectedContributions := []domain.Contribution{
|
||||
@ -50,7 +55,9 @@ func TestContributionRepository_ListByUserID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
userID := uint(1)
|
||||
|
||||
@ -69,7 +76,9 @@ func TestContributionRepository_ListByReviewerID(t *testing.T) {
|
||||
t.Run("should return contributions for a given reviewer id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
reviewerID := uint(1)
|
||||
expectedContributions := []domain.Contribution{
|
||||
@ -94,7 +103,9 @@ func TestContributionRepository_ListByReviewerID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
reviewerID := uint(1)
|
||||
|
||||
@ -113,7 +124,9 @@ func TestContributionRepository_ListByWorkID(t *testing.T) {
|
||||
t.Run("should return contributions for a given work id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
workID := uint(1)
|
||||
expectedContributions := []domain.Contribution{
|
||||
@ -138,7 +151,9 @@ func TestContributionRepository_ListByWorkID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
workID := uint(1)
|
||||
|
||||
@ -157,7 +172,9 @@ func TestContributionRepository_ListByTranslationID(t *testing.T) {
|
||||
t.Run("should return contributions for a given translation id", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
translationID := uint(1)
|
||||
expectedContributions := []domain.Contribution{
|
||||
@ -182,7 +199,9 @@ func TestContributionRepository_ListByTranslationID(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
translationID := uint(1)
|
||||
|
||||
@ -201,7 +220,9 @@ func TestContributionRepository_ListByStatus(t *testing.T) {
|
||||
t.Run("should return contributions for a given status", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
status := "draft"
|
||||
expectedContributions := []domain.Contribution{
|
||||
@ -226,7 +247,9 @@ func TestContributionRepository_ListByStatus(t *testing.T) {
|
||||
t.Run("should return error if query fails", func(t *testing.T) {
|
||||
db, mock, err := newMockDb()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
repo := repo.NewContributionRepository(db, cfg)
|
||||
|
||||
status := "draft"
|
||||
|
||||
@ -239,4 +262,4 @@ func TestContributionRepository_ListByStatus(t *testing.T) {
|
||||
assert.Nil(t, contributions)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type copyrightClaimRepository struct {
|
||||
}
|
||||
|
||||
// NewCopyrightClaimRepository creates a new CopyrightClaimRepository.
|
||||
func NewCopyrightClaimRepository(db *gorm.DB) domain.CopyrightClaimRepository {
|
||||
func NewCopyrightClaimRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightClaimRepository {
|
||||
return ©rightClaimRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("copyright_claim.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type copyrightRepository struct {
|
||||
}
|
||||
|
||||
// NewCopyrightRepository creates a new CopyrightRepository.
|
||||
func NewCopyrightRepository(db *gorm.DB) domain.CopyrightRepository {
|
||||
func NewCopyrightRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightRepository {
|
||||
return ©rightRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("copyright.repository"),
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
@ -41,7 +42,9 @@ func (s *CopyrightRepositoryTestSuite) SetupTest() {
|
||||
|
||||
s.db = gormDB
|
||||
s.mock = mock
|
||||
s.repo = sql.NewCopyrightRepository(s.db)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.repo = sql.NewCopyrightRepository(s.db, cfg)
|
||||
}
|
||||
|
||||
// TearDownTest checks if all expectations were met.
|
||||
@ -236,4 +239,4 @@ func (s *CopyrightRepositoryTestSuite) TestRemoveCopyrightFromSource() {
|
||||
err := s.repo.RemoveCopyrightFromSource(context.Background(), sourceID, copyrightID)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -14,9 +15,9 @@ type countryRepository struct {
|
||||
}
|
||||
|
||||
// NewCountryRepository creates a new CountryRepository.
|
||||
func NewCountryRepository(db *gorm.DB) domain.CountryRepository {
|
||||
func NewCountryRepository(db *gorm.DB, cfg *config.Config) domain.CountryRepository {
|
||||
return &countryRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Country](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Country](db, cfg),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type edgeRepository struct {
|
||||
}
|
||||
|
||||
// NewEdgeRepository creates a new EdgeRepository.
|
||||
func NewEdgeRepository(db *gorm.DB) domain.EdgeRepository {
|
||||
func NewEdgeRepository(db *gorm.DB, cfg *config.Config) domain.EdgeRepository {
|
||||
return &edgeRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Edge](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Edge](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("edge.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type editionRepository struct {
|
||||
}
|
||||
|
||||
// NewEditionRepository creates a new EditionRepository.
|
||||
func NewEditionRepository(db *gorm.DB) domain.EditionRepository {
|
||||
func NewEditionRepository(db *gorm.DB, cfg *config.Config) domain.EditionRepository {
|
||||
return &editionRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("edition.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
@ -18,9 +19,9 @@ type emailVerificationRepository struct {
|
||||
}
|
||||
|
||||
// NewEmailVerificationRepository creates a new EmailVerificationRepository.
|
||||
func NewEmailVerificationRepository(db *gorm.DB) domain.EmailVerificationRepository {
|
||||
func NewEmailVerificationRepository(db *gorm.DB, cfg *config.Config) domain.EmailVerificationRepository {
|
||||
return &emailVerificationRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("email_verification.repository"),
|
||||
}
|
||||
@ -69,4 +70,4 @@ func (r *emailVerificationRepository) MarkAsUsed(ctx context.Context, id uint) e
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type likeRepository struct {
|
||||
}
|
||||
|
||||
// NewLikeRepository creates a new LikeRepository.
|
||||
func NewLikeRepository(db *gorm.DB) domain.LikeRepository {
|
||||
func NewLikeRepository(db *gorm.DB, cfg *config.Config) domain.LikeRepository {
|
||||
return &likeRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Like](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Like](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("like.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/domain/localization"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -15,7 +16,7 @@ type localizationRepository struct {
|
||||
tracer trace.Tracer
|
||||
}
|
||||
|
||||
func NewLocalizationRepository(db *gorm.DB) localization.LocalizationRepository {
|
||||
func NewLocalizationRepository(db *gorm.DB, cfg *config.Config) localization.LocalizationRepository {
|
||||
return &localizationRepository{
|
||||
db: db,
|
||||
tracer: otel.Tracer("localization.repository"),
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type monetizationRepository struct {
|
||||
}
|
||||
|
||||
// NewMonetizationRepository creates a new MonetizationRepository.
|
||||
func NewMonetizationRepository(db *gorm.DB) domain.MonetizationRepository {
|
||||
func NewMonetizationRepository(db *gorm.DB, cfg *config.Config) domain.MonetizationRepository {
|
||||
return &monetizationRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Monetization](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("monetization.repository"),
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
workdomain "tercul/internal/domain/work"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -18,7 +19,9 @@ type MonetizationRepositoryTestSuite struct {
|
||||
|
||||
func (s *MonetizationRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
s.MonetizationRepo = sql.NewMonetizationRepository(s.DB)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.MonetizationRepo = sql.NewMonetizationRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *MonetizationRepositoryTestSuite) SetupTest() {
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
@ -18,9 +19,9 @@ type passwordResetRepository struct {
|
||||
}
|
||||
|
||||
// NewPasswordResetRepository creates a new PasswordResetRepository.
|
||||
func NewPasswordResetRepository(db *gorm.DB) domain.PasswordResetRepository {
|
||||
func NewPasswordResetRepository(db *gorm.DB, cfg *config.Config) domain.PasswordResetRepository {
|
||||
return &passwordResetRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("password_reset.repository"),
|
||||
}
|
||||
@ -69,4 +70,4 @@ func (r *passwordResetRepository) MarkAsUsed(ctx context.Context, id uint) error
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"math"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type placeRepository struct {
|
||||
}
|
||||
|
||||
// NewPlaceRepository creates a new PlaceRepository.
|
||||
func NewPlaceRepository(db *gorm.DB) domain.PlaceRepository {
|
||||
func NewPlaceRepository(db *gorm.DB, cfg *config.Config) domain.PlaceRepository {
|
||||
return &placeRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Place](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Place](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("place.repository"),
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -16,9 +17,9 @@ type publisherRepository struct {
|
||||
}
|
||||
|
||||
// NewPublisherRepository creates a new PublisherRepository.
|
||||
func NewPublisherRepository(db *gorm.DB) domain.PublisherRepository {
|
||||
func NewPublisherRepository(db *gorm.DB, cfg *config.Config) domain.PublisherRepository {
|
||||
return &publisherRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Publisher](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("publisher.repository"),
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"tercul/internal/domain/auth"
|
||||
"tercul/internal/domain/localization"
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -26,31 +27,33 @@ type Repositories struct {
|
||||
Source domain.SourceRepository
|
||||
Copyright domain.CopyrightRepository
|
||||
Monetization domain.MonetizationRepository
|
||||
Contribution domain.ContributionRepository
|
||||
Analytics analytics.Repository
|
||||
Auth auth.AuthRepository
|
||||
Localization localization.LocalizationRepository
|
||||
}
|
||||
|
||||
// NewRepositories creates a new Repositories container
|
||||
func NewRepositories(db *gorm.DB) *Repositories {
|
||||
func NewRepositories(db *gorm.DB, cfg *config.Config) *Repositories {
|
||||
return &Repositories{
|
||||
Work: NewWorkRepository(db),
|
||||
User: NewUserRepository(db),
|
||||
Author: NewAuthorRepository(db),
|
||||
Translation: NewTranslationRepository(db),
|
||||
Comment: NewCommentRepository(db),
|
||||
Like: NewLikeRepository(db),
|
||||
Bookmark: NewBookmarkRepository(db),
|
||||
Collection: NewCollectionRepository(db),
|
||||
Tag: NewTagRepository(db),
|
||||
Category: NewCategoryRepository(db),
|
||||
Book: NewBookRepository(db),
|
||||
Publisher: NewPublisherRepository(db),
|
||||
Source: NewSourceRepository(db),
|
||||
Copyright: NewCopyrightRepository(db),
|
||||
Monetization: NewMonetizationRepository(db),
|
||||
Analytics: NewAnalyticsRepository(db),
|
||||
Auth: NewAuthRepository(db),
|
||||
Localization: NewLocalizationRepository(db),
|
||||
Work: NewWorkRepository(db, cfg),
|
||||
User: NewUserRepository(db, cfg),
|
||||
Author: NewAuthorRepository(db, cfg),
|
||||
Translation: NewTranslationRepository(db, cfg),
|
||||
Comment: NewCommentRepository(db, cfg),
|
||||
Like: NewLikeRepository(db, cfg),
|
||||
Bookmark: NewBookmarkRepository(db, cfg),
|
||||
Collection: NewCollectionRepository(db, cfg),
|
||||
Tag: NewTagRepository(db, cfg),
|
||||
Category: NewCategoryRepository(db, cfg),
|
||||
Book: NewBookRepository(db, cfg),
|
||||
Publisher: NewPublisherRepository(db, cfg),
|
||||
Source: NewSourceRepository(db, cfg),
|
||||
Copyright: NewCopyrightRepository(db, cfg),
|
||||
Monetization: NewMonetizationRepository(db, cfg),
|
||||
Contribution: NewContributionRepository(db, cfg),
|
||||
Analytics: NewAnalyticsRepository(db, cfg),
|
||||
Auth: NewAuthRepository(db, cfg),
|
||||
Localization: NewLocalizationRepository(db, cfg),
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type sourceRepository struct {
|
||||
}
|
||||
|
||||
// NewSourceRepository creates a new SourceRepository.
|
||||
func NewSourceRepository(db *gorm.DB) domain.SourceRepository {
|
||||
func NewSourceRepository(db *gorm.DB, cfg *config.Config) domain.SourceRepository {
|
||||
return &sourceRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Source](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Source](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("source.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type tagRepository struct {
|
||||
}
|
||||
|
||||
// NewTagRepository creates a new TagRepository.
|
||||
func NewTagRepository(db *gorm.DB) domain.TagRepository {
|
||||
func NewTagRepository(db *gorm.DB, cfg *config.Config) domain.TagRepository {
|
||||
return &tagRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("tag.repository"),
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package sql
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type translationRepository struct {
|
||||
}
|
||||
|
||||
// NewTranslationRepository creates a new TranslationRepository.
|
||||
func NewTranslationRepository(db *gorm.DB) domain.TranslationRepository {
|
||||
func NewTranslationRepository(db *gorm.DB, cfg *config.Config) domain.TranslationRepository {
|
||||
return &translationRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Translation](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("translation.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type userProfileRepository struct {
|
||||
}
|
||||
|
||||
// NewUserProfileRepository creates a new UserProfileRepository.
|
||||
func NewUserProfileRepository(db *gorm.DB) domain.UserProfileRepository {
|
||||
func NewUserProfileRepository(db *gorm.DB, cfg *config.Config) domain.UserProfileRepository {
|
||||
return &userProfileRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user_profile.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -17,9 +18,9 @@ type userRepository struct {
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new UserRepository.
|
||||
func NewUserRepository(db *gorm.DB) domain.UserRepository {
|
||||
func NewUserRepository(db *gorm.DB, cfg *config.Config) domain.UserRepository {
|
||||
return &userRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.User](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.User](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user.repository"),
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
@ -18,9 +19,9 @@ type userSessionRepository struct {
|
||||
}
|
||||
|
||||
// NewUserSessionRepository creates a new UserSessionRepository.
|
||||
func NewUserSessionRepository(db *gorm.DB) domain.UserSessionRepository {
|
||||
func NewUserSessionRepository(db *gorm.DB, cfg *config.Config) domain.UserSessionRepository {
|
||||
return &userSessionRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user_session.repository"),
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@ -19,9 +20,9 @@ type workRepository struct {
|
||||
}
|
||||
|
||||
// NewWorkRepository creates a new WorkRepository.
|
||||
func NewWorkRepository(db *gorm.DB) work.WorkRepository {
|
||||
func NewWorkRepository(db *gorm.DB, cfg *config.Config) work.WorkRepository {
|
||||
return &workRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[work.Work](db),
|
||||
BaseRepository: NewBaseRepositoryImpl[work.Work](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("work.repository"),
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -18,7 +19,9 @@ type WorkRepositoryTestSuite struct {
|
||||
|
||||
func (s *WorkRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
s.WorkRepo = sql.NewWorkRepository(s.DB)
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.WorkRepo = sql.NewWorkRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *WorkRepositoryTestSuite) TestCreateWork() {
|
||||
|
||||
@ -31,9 +31,8 @@ type MemoryAnalysisCache struct {
|
||||
}
|
||||
|
||||
// NewMemoryAnalysisCache creates a new MemoryAnalysisCache
|
||||
func NewMemoryAnalysisCache(enabled bool) *MemoryAnalysisCache {
|
||||
// capacity from config
|
||||
cap := config.Cfg.NLPMemoryCacheCap
|
||||
func NewMemoryAnalysisCache(cfg *config.Config, enabled bool) *MemoryAnalysisCache {
|
||||
cap := cfg.NLPMemoryCacheCap
|
||||
if cap <= 0 {
|
||||
cap = 1024
|
||||
}
|
||||
@ -82,13 +81,19 @@ func (c *MemoryAnalysisCache) IsEnabled() bool {
|
||||
type RedisAnalysisCache struct {
|
||||
cache cache.Cache
|
||||
enabled bool
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewRedisAnalysisCache creates a new RedisAnalysisCache
|
||||
func NewRedisAnalysisCache(cache cache.Cache, enabled bool) *RedisAnalysisCache {
|
||||
func NewRedisAnalysisCache(cfg *config.Config, cache cache.Cache, enabled bool) *RedisAnalysisCache {
|
||||
ttlSeconds := cfg.NLPRedisCacheTTLSeconds
|
||||
if ttlSeconds <= 0 {
|
||||
ttlSeconds = 3600 // default 1 hour
|
||||
}
|
||||
return &RedisAnalysisCache{
|
||||
cache: cache,
|
||||
enabled: enabled,
|
||||
ttl: time.Duration(ttlSeconds) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,9 +118,7 @@ func (c *RedisAnalysisCache) Set(ctx context.Context, key string, result *Analys
|
||||
return nil
|
||||
}
|
||||
|
||||
// TTL from config
|
||||
ttlSeconds := config.Cfg.NLPRedisCacheTTLSeconds
|
||||
err := c.cache.Set(ctx, key, result, time.Duration(ttlSeconds)*time.Second)
|
||||
err := c.cache.Set(ctx, key, result, c.ttl)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).With("key", key).Error(err, "Failed to cache analysis result")
|
||||
return err
|
||||
@ -189,4 +192,4 @@ func (c *CompositeAnalysisCache) Set(ctx context.Context, key string, result *An
|
||||
// IsEnabled returns whether caching is enabled
|
||||
func (c *CompositeAnalysisCache) IsEnabled() bool {
|
||||
return c.enabled
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@ type LinguisticsFactory struct {
|
||||
|
||||
// NewLinguisticsFactory creates a new LinguisticsFactory with all components
|
||||
func NewLinguisticsFactory(
|
||||
cfg *config.Config,
|
||||
db *gorm.DB,
|
||||
cache cache.Cache,
|
||||
concurrency int,
|
||||
@ -32,18 +33,18 @@ func NewLinguisticsFactory(
|
||||
textAnalyzer = textAnalyzer.WithSentimentProvider(sentimentProvider)
|
||||
|
||||
// Wire language detector: lingua-go (configurable)
|
||||
if config.Cfg.NLPUseLingua {
|
||||
if cfg.NLPUseLingua {
|
||||
textAnalyzer = textAnalyzer.WithLanguageDetector(NewLinguaLanguageDetector())
|
||||
}
|
||||
|
||||
// Wire keyword provider: lightweight TF-IDF approximation (configurable)
|
||||
if config.Cfg.NLPUseTFIDF {
|
||||
if cfg.NLPUseTFIDF {
|
||||
textAnalyzer = textAnalyzer.WithKeywordProvider(NewTFIDFKeywordProvider())
|
||||
}
|
||||
|
||||
// Create cache components
|
||||
memoryCache := NewMemoryAnalysisCache(cacheEnabled)
|
||||
redisCache := NewRedisAnalysisCache(cache, cacheEnabled)
|
||||
memoryCache := NewMemoryAnalysisCache(cfg, cacheEnabled)
|
||||
redisCache := NewRedisAnalysisCache(cfg, cache, cacheEnabled)
|
||||
analysisCache := NewCompositeAnalysisCache(memoryCache, redisCache, cacheEnabled)
|
||||
|
||||
// Create repository
|
||||
@ -105,4 +106,4 @@ func (f *LinguisticsFactory) GetAnalyzer() Analyzer {
|
||||
// GetSentimentProvider returns the sentiment provider
|
||||
func (f *LinguisticsFactory) GetSentimentProvider() SentimentProvider {
|
||||
return f.sentimentProvider
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
package linguistics
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"tercul/internal/platform/config"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFactory_WiresProviders(t *testing.T) {
|
||||
// We won't spin a DB/cache here; this is a smoke test of wiring methods
|
||||
f := NewLinguisticsFactory(nil, nil, 2, true, nil)
|
||||
cfg, err := config.LoadConfig()
|
||||
require.NoError(t, err)
|
||||
f := NewLinguisticsFactory(cfg, nil, nil, 2, true, nil)
|
||||
ta := f.GetTextAnalyzer().(*BasicTextAnalyzer)
|
||||
require.NotNil(t, ta)
|
||||
}
|
||||
}
|
||||
@ -17,8 +17,8 @@ type BatchProcessor struct {
|
||||
}
|
||||
|
||||
// NewBatchProcessor creates a new BatchProcessor
|
||||
func NewBatchProcessor(db *gorm.DB) *BatchProcessor {
|
||||
batchSize := config.Cfg.BatchSize
|
||||
func NewBatchProcessor(db *gorm.DB, cfg *config.Config) *BatchProcessor {
|
||||
batchSize := cfg.BatchSize
|
||||
if batchSize <= 0 {
|
||||
batchSize = DefaultBatchSize
|
||||
}
|
||||
|
||||
@ -54,6 +54,6 @@ func (s *SyncJob) SyncEdgesBatch(ctx context.Context, batchSize, offset int) err
|
||||
edgeMaps = append(edgeMaps, edgeMap)
|
||||
}
|
||||
|
||||
batchProcessor := NewBatchProcessor(s.DB)
|
||||
batchProcessor := NewBatchProcessor(s.DB, s.Cfg)
|
||||
return batchProcessor.CreateObjectsBatch(ctx, "Edge", edgeMaps)
|
||||
}
|
||||
|
||||
@ -76,6 +76,6 @@ func (s *SyncJob) SyncAllEntities(ctx context.Context) error {
|
||||
|
||||
// syncEntities is a generic function to sync a given entity type.
|
||||
func (s *SyncJob) syncEntities(className string, ctx context.Context) error {
|
||||
batchProcessor := NewBatchProcessor(s.DB)
|
||||
batchProcessor := NewBatchProcessor(s.DB, s.Cfg)
|
||||
return batchProcessor.ProcessAllEntities(ctx, className)
|
||||
}
|
||||
|
||||
@ -64,6 +64,6 @@ func RegisterQueueHandlers(srv *asynq.Server, syncJob *SyncJob) {
|
||||
mux.HandleFunc(TaskEntitySync, syncJob.HandleEntitySync)
|
||||
mux.HandleFunc(TaskEdgeSync, syncJob.HandleEdgeSync)
|
||||
if err := srv.Run(mux); err != nil {
|
||||
log.Fatalf("Failed to start asynq server: %v", err)
|
||||
log.Printf("Failed to start asynq server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package sync
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"gorm.io/gorm"
|
||||
@ -12,13 +13,15 @@ import (
|
||||
type SyncJob struct {
|
||||
DB *gorm.DB
|
||||
AsynqClient *asynq.Client
|
||||
Cfg *config.Config
|
||||
}
|
||||
|
||||
// NewSyncJob initializes a new SyncJob.
|
||||
func NewSyncJob(db *gorm.DB, aClient *asynq.Client) *SyncJob {
|
||||
func NewSyncJob(db *gorm.DB, aClient *asynq.Client, cfg *config.Config) *SyncJob {
|
||||
return &SyncJob{
|
||||
DB: db,
|
||||
AsynqClient: aClient,
|
||||
Cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -41,16 +41,17 @@ type JWTManager struct {
|
||||
}
|
||||
|
||||
// NewJWTManager creates a new JWT manager
|
||||
func NewJWTManager() *JWTManager {
|
||||
secretKey := config.Cfg.JWTSecret
|
||||
func NewJWTManager(cfg *config.Config) *JWTManager {
|
||||
secretKey := cfg.JWTSecret
|
||||
if secretKey == "" {
|
||||
secretKey = "default-secret-key-change-in-production"
|
||||
}
|
||||
|
||||
duration := config.Cfg.JWTExpiration
|
||||
if duration == 0 {
|
||||
duration = 24 * time.Hour // Default to 24 hours
|
||||
durationInHours := cfg.JWTExpiration
|
||||
if durationInHours <= 0 {
|
||||
durationInHours = 24 // Default to 24 hours
|
||||
}
|
||||
duration := time.Duration(durationInHours) * time.Hour
|
||||
|
||||
return &JWTManager{
|
||||
secretKey: []byte(secretKey),
|
||||
|
||||
@ -54,7 +54,7 @@ func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// RoleMiddleware creates middleware for role-based authorization
|
||||
func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
|
||||
func RoleMiddleware(jwtManager *JWTManager, requiredRole string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger := log.FromContext(r.Context())
|
||||
@ -65,7 +65,6 @@ func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
jwtManager := NewJWTManager()
|
||||
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
||||
logger.With("user_role", claims.Role).With("required_role", requiredRole).Warn("Authorization failed - insufficient role")
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
@ -142,13 +141,12 @@ func RequireAuth(ctx context.Context) (*Claims, error) {
|
||||
}
|
||||
|
||||
// RequireRole ensures the user has the required role
|
||||
func RequireRole(ctx context.Context, requiredRole string) (*Claims, error) {
|
||||
func RequireRole(ctx context.Context, jwtManager *JWTManager, requiredRole string) (*Claims, error) {
|
||||
claims, err := RequireAuth(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jwtManager := NewJWTManager()
|
||||
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -6,24 +6,29 @@ import (
|
||||
|
||||
// Config stores all configuration of the application.
|
||||
type Config struct {
|
||||
Environment string `mapstructure:"ENVIRONMENT"`
|
||||
ServerPort string `mapstructure:"SERVER_PORT"`
|
||||
DBHost string `mapstructure:"DB_HOST"`
|
||||
DBPort string `mapstructure:"DB_PORT"`
|
||||
DBUser string `mapstructure:"DB_USER"`
|
||||
DBPassword string `mapstructure:"DB_PASSWORD"`
|
||||
DBName string `mapstructure:"DB_NAME"`
|
||||
JWTSecret string `mapstructure:"JWT_SECRET"`
|
||||
JWTExpiration int `mapstructure:"JWT_EXPIRATION_HOURS"`
|
||||
WeaviateHost string `mapstructure:"WEAVIATE_HOST"`
|
||||
WeaviateScheme string `mapstructure:"WEAVIATE_SCHEME"`
|
||||
MigrationPath string `mapstructure:"MIGRATION_PATH"`
|
||||
RedisAddr string `mapstructure:"REDIS_ADDR"`
|
||||
RedisPassword string `mapstructure:"REDIS_PASSWORD"`
|
||||
RedisDB int `mapstructure:"REDIS_DB"`
|
||||
SyncBatchSize int `mapstructure:"SYNC_BATCH_SIZE"`
|
||||
RateLimit int `mapstructure:"RATE_LIMIT"`
|
||||
RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"`
|
||||
Environment string `mapstructure:"ENVIRONMENT"`
|
||||
ServerPort string `mapstructure:"SERVER_PORT"`
|
||||
DBHost string `mapstructure:"DB_HOST"`
|
||||
DBPort string `mapstructure:"DB_PORT"`
|
||||
DBUser string `mapstructure:"DB_USER"`
|
||||
DBPassword string `mapstructure:"DB_PASSWORD"`
|
||||
DBName string `mapstructure:"DB_NAME"`
|
||||
JWTSecret string `mapstructure:"JWT_SECRET"`
|
||||
JWTExpiration int `mapstructure:"JWT_EXPIRATION_HOURS"`
|
||||
WeaviateHost string `mapstructure:"WEAVIATE_HOST"`
|
||||
WeaviateScheme string `mapstructure:"WEAVIATE_SCHEME"`
|
||||
MigrationPath string `mapstructure:"MIGRATION_PATH"`
|
||||
RedisAddr string `mapstructure:"REDIS_ADDR"`
|
||||
RedisPassword string `mapstructure:"REDIS_PASSWORD"`
|
||||
RedisDB int `mapstructure:"REDIS_DB"`
|
||||
BatchSize int `mapstructure:"BATCH_SIZE"`
|
||||
RateLimit int `mapstructure:"RATE_LIMIT"`
|
||||
RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"`
|
||||
PageSize int `mapstructure:"PAGE_SIZE"`
|
||||
NLPMemoryCacheCap int `mapstructure:"NLP_MEMORY_CACHE_CAP"`
|
||||
NLPRedisCacheTTLSeconds int `mapstructure:"NLP_REDIS_CACHE_TTL_SECONDS"`
|
||||
NLPUseLingua bool `mapstructure:"NLP_USE_LINGUA"`
|
||||
NLPUseTFIDF bool `mapstructure:"NLP_USE_TFIDF"`
|
||||
}
|
||||
|
||||
// LoadConfig reads configuration from file or environment variables.
|
||||
@ -43,9 +48,13 @@ func LoadConfig() (*Config, error) {
|
||||
viper.SetDefault("REDIS_ADDR", "localhost:6379")
|
||||
viper.SetDefault("REDIS_PASSWORD", "")
|
||||
viper.SetDefault("REDIS_DB", 0)
|
||||
viper.SetDefault("SYNC_BATCH_SIZE", 100)
|
||||
viper.SetDefault("BATCH_SIZE", 100)
|
||||
viper.SetDefault("RATE_LIMIT", 10)
|
||||
viper.SetDefault("RATE_LIMIT_BURST", 100)
|
||||
viper.SetDefault("NLP_MEMORY_CACHE_CAP", 1024)
|
||||
viper.SetDefault("NLP_REDIS_CACHE_TTL_SECONDS", 3600)
|
||||
viper.SetDefault("NLP_USE_LINGUA", true)
|
||||
viper.SetDefault("NLP_USE_TFIDF", true)
|
||||
|
||||
viper.AutomaticEnv()
|
||||
|
||||
|
||||
@ -21,10 +21,12 @@ type RateLimiter struct {
|
||||
}
|
||||
|
||||
// NewRateLimiter creates a new rate limiter
|
||||
func NewRateLimiter(rate, capacity int) *RateLimiter {
|
||||
func NewRateLimiter(cfg *config.Config) *RateLimiter {
|
||||
rate := cfg.RateLimit
|
||||
if rate <= 0 {
|
||||
rate = 10 // default rate: 10 requests per second
|
||||
}
|
||||
capacity := cfg.RateLimitBurst
|
||||
if capacity <= 0 {
|
||||
capacity = 100 // default capacity: 100 tokens
|
||||
}
|
||||
@ -73,28 +75,29 @@ func minF(a, b float64) float64 {
|
||||
}
|
||||
|
||||
// RateLimitMiddleware creates a middleware that applies rate limiting
|
||||
func RateLimitMiddleware(next http.Handler) http.Handler {
|
||||
rateLimiter := NewRateLimiter(config.Cfg.RateLimit, config.Cfg.RateLimitBurst)
|
||||
func RateLimitMiddleware(cfg *config.Config) func(http.Handler) http.Handler {
|
||||
rateLimiter := NewRateLimiter(cfg)
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Use X-Client-ID header for client identification in tests
|
||||
clientID := r.Header.Get("X-Client-ID")
|
||||
if clientID == "" {
|
||||
clientID = r.RemoteAddr
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Use X-Client-ID header for client identification in tests
|
||||
clientID := r.Header.Get("X-Client-ID")
|
||||
if clientID == "" {
|
||||
clientID = r.RemoteAddr
|
||||
}
|
||||
// Check if request is allowed
|
||||
if !rateLimiter.Allow(clientID) {
|
||||
log.FromContext(r.Context()).
|
||||
With("clientID", clientID).
|
||||
Warn("Rate limit exceeded")
|
||||
|
||||
// Check if request is allowed
|
||||
if !rateLimiter.Allow(clientID) {
|
||||
log.FromContext(r.Context()).
|
||||
With("clientID", clientID).
|
||||
Warn("Rate limit exceeded")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
w.Write([]byte("Rate limit exceeded. Please try again later."))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
w.Write([]byte("Rate limit exceeded. Please try again later."))
|
||||
return
|
||||
}
|
||||
|
||||
// Continue to the next handler
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
// Continue to the next handler
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,9 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tercul/internal/platform/config"
|
||||
platformhttp "tercul/internal/platform/http"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -20,8 +19,8 @@ type RateLimiterSuite struct {
|
||||
|
||||
// TestRateLimiter tests the RateLimiter
|
||||
func (s *RateLimiterSuite) TestRateLimiter() {
|
||||
// Create a new rate limiter with 2 requests per second and a burst of 3
|
||||
limiter := platformhttp.NewRateLimiter(2, 3)
|
||||
cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
|
||||
limiter := platformhttp.NewRateLimiter(cfg)
|
||||
|
||||
// Test that the first 3 requests are allowed (burst)
|
||||
for i := 0; i < 3; i++ {
|
||||
@ -49,8 +48,8 @@ func (s *RateLimiterSuite) TestRateLimiter() {
|
||||
|
||||
// TestRateLimiterMultipleClients tests the RateLimiter with multiple clients
|
||||
func (s *RateLimiterSuite) TestRateLimiterMultipleClients() {
|
||||
// Create a new rate limiter with 2 requests per second and a burst of 3
|
||||
limiter := platformhttp.NewRateLimiter(2, 3)
|
||||
cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
|
||||
limiter := platformhttp.NewRateLimiter(cfg)
|
||||
|
||||
// Test that the first 3 requests for client1 are allowed (burst)
|
||||
for i := 0; i < 3; i++ {
|
||||
@ -75,17 +74,15 @@ func (s *RateLimiterSuite) TestRateLimiterMultipleClients() {
|
||||
|
||||
// TestRateLimiterMiddleware tests the RateLimiterMiddleware
|
||||
func (s *RateLimiterSuite) TestRateLimiterMiddleware() {
|
||||
// Set config to match test expectations
|
||||
config.Cfg.RateLimit = 2
|
||||
config.Cfg.RateLimitBurst = 3
|
||||
cfg := &config.Config{RateLimit: 2, RateLimitBurst: 3}
|
||||
|
||||
// Create a test handler that always returns 200 OK
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Create a rate limiter middleware with 2 requests per second and a burst of 3
|
||||
middleware := platformhttp.RateLimitMiddleware(testHandler)
|
||||
// Create a rate limiter middleware
|
||||
middleware := platformhttp.RateLimitMiddleware(cfg)(testHandler)
|
||||
|
||||
// Create a test server
|
||||
server := httptest.NewServer(middleware)
|
||||
@ -144,22 +141,22 @@ func TestRateLimiterSuite(t *testing.T) {
|
||||
// TestNewRateLimiter tests the NewRateLimiter function
|
||||
func TestNewRateLimiter(t *testing.T) {
|
||||
// Test with valid parameters
|
||||
limiter := platformhttp.NewRateLimiter(10, 20)
|
||||
limiter := platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: 20})
|
||||
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter")
|
||||
|
||||
// Test with zero rate (should use default)
|
||||
limiter = platformhttp.NewRateLimiter(0, 20)
|
||||
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 0, RateLimitBurst: 20})
|
||||
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate")
|
||||
|
||||
// Test with zero capacity (should use default)
|
||||
limiter = platformhttp.NewRateLimiter(10, 0)
|
||||
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: 0})
|
||||
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity")
|
||||
|
||||
// Test with negative rate (should use default)
|
||||
limiter = platformhttp.NewRateLimiter(-10, 20)
|
||||
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: -10, RateLimitBurst: 20})
|
||||
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default rate")
|
||||
|
||||
// Test with negative capacity (should use default)
|
||||
limiter = platformhttp.NewRateLimiter(10, -20)
|
||||
limiter = platformhttp.NewRateLimiter(&config.Config{RateLimit: 10, RateLimitBurst: -20})
|
||||
assert.NotNil(t, limiter, "NewRateLimiter should return a non-nil limiter with default capacity")
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,10 @@ import (
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/domain/search"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/domain/work"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
platform_config "tercul/internal/platform/config"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -106,21 +107,21 @@ func DefaultTestConfig() *TestConfig {
|
||||
}
|
||||
|
||||
// SetupSuite sets up the test suite with the specified configuration
|
||||
func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
|
||||
if config == nil {
|
||||
config = DefaultTestConfig()
|
||||
func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
|
||||
if testConfig == nil {
|
||||
testConfig = DefaultTestConfig()
|
||||
}
|
||||
|
||||
var dbPath string
|
||||
if !config.UseInMemoryDB && config.DBPath != "" {
|
||||
if !testConfig.UseInMemoryDB && testConfig.DBPath != "" {
|
||||
// Clean up previous test database file before starting
|
||||
_ = os.Remove(config.DBPath)
|
||||
_ = os.Remove(testConfig.DBPath)
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(config.DBPath)
|
||||
dir := filepath.Dir(testConfig.DBPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
s.T().Fatalf("Failed to create database directory: %v", err)
|
||||
}
|
||||
dbPath = config.DBPath
|
||||
dbPath = testConfig.DBPath
|
||||
} else {
|
||||
// Use in-memory database
|
||||
dbPath = ":memory:"
|
||||
@ -131,7 +132,7 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second,
|
||||
LogLevel: config.LogLevel,
|
||||
LogLevel: testConfig.LogLevel,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: false,
|
||||
},
|
||||
@ -155,7 +156,12 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
|
||||
&domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{},
|
||||
)
|
||||
|
||||
repos := sql.NewRepositories(s.DB)
|
||||
cfg, err := platform_config.LoadConfig()
|
||||
if err != nil {
|
||||
s.T().Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
repos := sql.NewRepositories(s.DB, cfg)
|
||||
var searchClient search.SearchClient = &mockSearchClient{}
|
||||
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
|
||||
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
||||
@ -163,30 +169,31 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
|
||||
s.T().Fatalf("Failed to create sentiment provider: %v", err)
|
||||
}
|
||||
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
|
||||
jwtManager := platform_auth.NewJWTManager()
|
||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||
|
||||
deps := app.Dependencies{
|
||||
WorkRepo: repos.Work,
|
||||
UserRepo: repos.User,
|
||||
AuthorRepo: repos.Author,
|
||||
TranslationRepo: repos.Translation,
|
||||
CommentRepo: repos.Comment,
|
||||
LikeRepo: repos.Like,
|
||||
BookmarkRepo: repos.Bookmark,
|
||||
CollectionRepo: repos.Collection,
|
||||
TagRepo: repos.Tag,
|
||||
CategoryRepo: repos.Category,
|
||||
BookRepo: repos.Book,
|
||||
PublisherRepo: repos.Publisher,
|
||||
SourceRepo: repos.Source,
|
||||
CopyrightRepo: repos.Copyright,
|
||||
MonetizationRepo: repos.Monetization,
|
||||
AnalyticsRepo: repos.Analytics,
|
||||
AuthRepo: repos.Auth,
|
||||
LocalizationRepo: repos.Localization,
|
||||
SearchClient: searchClient,
|
||||
AnalyticsService: analyticsService,
|
||||
JWTManager: jwtManager,
|
||||
WorkRepo: repos.Work,
|
||||
UserRepo: repos.User,
|
||||
AuthorRepo: repos.Author,
|
||||
TranslationRepo: repos.Translation,
|
||||
CommentRepo: repos.Comment,
|
||||
LikeRepo: repos.Like,
|
||||
BookmarkRepo: repos.Bookmark,
|
||||
CollectionRepo: repos.Collection,
|
||||
TagRepo: repos.Tag,
|
||||
CategoryRepo: repos.Category,
|
||||
BookRepo: repos.Book,
|
||||
PublisherRepo: repos.Publisher,
|
||||
SourceRepo: repos.Source,
|
||||
CopyrightRepo: repos.Copyright,
|
||||
MonetizationRepo: repos.Monetization,
|
||||
ContributionRepo: repos.Contribution,
|
||||
AnalyticsRepo: repos.Analytics,
|
||||
AuthRepo: repos.Auth,
|
||||
LocalizationRepo: repos.Localization,
|
||||
SearchClient: searchClient,
|
||||
AnalyticsService: analyticsService,
|
||||
JWTManager: jwtManager,
|
||||
}
|
||||
s.App = app.NewApplication(deps)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user