Merge pull request #16 from SamyRai/fix/complete-pending-tasks

Fix: Complete pending tasks and improve code quality
This commit is contained in:
Damir Mukimov 2025-10-05 17:18:10 +02:00 committed by GitHub
commit 37a007b08c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 223 additions and 257 deletions

View File

@ -112,84 +112,83 @@ func main() {
log.Fatal(err, "Failed to create sentiment provider") log.Fatal(err, "Failed to create sentiment provider")
} }
// Create platform components
jwtManager := auth.NewJWTManager()
// Create application services // Create application services
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
// Create application dependencies
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,
}
// Create application // Create application
application := app.NewApplication(repos, searchClient, analyticsService) application := app.NewApplication(deps)
// Create GraphQL server // Create GraphQL server
resolver := &graph.Resolver{ resolver := &graph.Resolver{
App: application, App: application,
} }
jwtManager := auth.NewJWTManager() // Create handlers
srv := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger) apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
graphQLServer := &http.Server{ playgroundHandler := playground.Handler("GraphQL Playground", "/query")
metricsHandler := observability.PrometheusHandler(reg)
// Consolidate handlers into a single mux
mux := http.NewServeMux()
mux.Handle("/query", apiHandler)
mux.Handle("/playground", playgroundHandler)
mux.Handle("/metrics", metricsHandler)
// Create a single HTTP server
mainServer := &http.Server{
Addr: config.Cfg.ServerPort, Addr: config.Cfg.ServerPort,
Handler: srv, Handler: mux,
} }
log.Info(fmt.Sprintf("GraphQL server created successfully on port %s", config.Cfg.ServerPort)) log.Info(fmt.Sprintf("API server listening on port %s", config.Cfg.ServerPort))
// Create GraphQL playground // Start the main server in a goroutine
playgroundHandler := playground.Handler("GraphQL", "/query")
playgroundServer := &http.Server{
Addr: config.Cfg.PlaygroundPort,
Handler: playgroundHandler,
}
log.Info(fmt.Sprintf("GraphQL playground created successfully on port %s", config.Cfg.PlaygroundPort))
// Create metrics server
metricsServer := &http.Server{
Addr: ":9090",
Handler: observability.PrometheusHandler(reg),
}
log.Info("Metrics server created successfully on port :9090")
// Start HTTP servers in goroutines
go func() { go func() {
log.Info(fmt.Sprintf("Starting GraphQL server on port %s", config.Cfg.ServerPort)) if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := graphQLServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err, "Failed to start server")
log.Fatal(err, "Failed to start GraphQL server")
} }
}() }()
go func() { // Wait for interrupt signal to gracefully shutdown the server
log.Info(fmt.Sprintf("Starting GraphQL playground on port %s", config.Cfg.PlaygroundPort))
if err := playgroundServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start GraphQL playground")
}
}()
go func() {
log.Info("Starting metrics server on port :9090")
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err, "Failed to start metrics server")
}
}()
// Wait for interrupt signal to gracefully shutdown the servers
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Info("Shutting down server...")
log.Info("Shutting down servers...")
// Graceful shutdown // Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
if err := graphQLServer.Shutdown(ctx); err != nil { if err := mainServer.Shutdown(ctx); err != nil {
log.Error(err, "GraphQL server forced to shutdown") log.Error(err, "Server forced to shutdown")
} }
if err := playgroundServer.Shutdown(ctx); err != nil { log.Info("Server shut down successfully")
log.Error(err, "GraphQL playground forced to shutdown")
}
if err := metricsServer.Shutdown(ctx); err != nil {
log.Error(err, "Metrics server forced to shutdown")
}
log.Info("All servers shutdown successfully")
} }

11
go.mod
View File

@ -17,6 +17,7 @@ require (
github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_golang v1.20.5
github.com/redis/go-redis/v9 v9.13.0 github.com/redis/go-redis/v9 v9.13.0
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/vektah/gqlparser/v2 v2.5.30 github.com/vektah/gqlparser/v2 v2.5.30
github.com/weaviate/weaviate v1.33.0-rc.1 github.com/weaviate/weaviate v1.33.0-rc.1
@ -47,6 +48,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/go-sysinfo v1.15.4 // indirect github.com/elastic/go-sysinfo v1.15.4 // indirect
github.com/elastic/go-windows v1.0.2 // indirect github.com/elastic/go-windows v1.0.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect github.com/go-faster/errors v0.7.1 // indirect
@ -92,6 +94,7 @@ require (
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/paulmach/orb v0.11.1 // indirect github.com/paulmach/orb v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@ -101,12 +104,17 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect github.com/sosodev/duration v1.3.1 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/vertica/vertica-sql-go v1.3.3 // indirect github.com/vertica/vertica-sql-go v1.3.3 // indirect
@ -118,6 +126,7 @@ require (
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.26.0 // indirect golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.42.0 // indirect

22
go.sum
View File

@ -94,6 +94,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -315,6 +317,8 @@ github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pemistahl/lingua-go v1.4.0 h1:ifYhthrlW7iO4icdubwlduYnmwU37V1sbNrwhKBR4rM= github.com/pemistahl/lingua-go v1.4.0 h1:ifYhthrlW7iO4icdubwlduYnmwU37V1sbNrwhKBR4rM=
github.com/pemistahl/lingua-go v1.4.0/go.mod h1:ECuM1Hp/3hvyh7k8aWSqNCPlTxLemFZsRjocUf3KgME= github.com/pemistahl/lingua-go v1.4.0/go.mod h1:ECuM1Hp/3hvyh7k8aWSqNCPlTxLemFZsRjocUf3KgME=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
@ -359,6 +363,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
@ -372,10 +378,18 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
@ -387,6 +401,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
@ -445,6 +461,8 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=

View File

@ -15,13 +15,41 @@ import (
"tercul/internal/app/user" "tercul/internal/app/user"
"tercul/internal/app/auth" "tercul/internal/app/auth"
"tercul/internal/app/work" "tercul/internal/app/work"
"tercul/internal/data/sql" "tercul/internal/domain"
auth_domain "tercul/internal/domain/auth"
localization_domain "tercul/internal/domain/localization"
"tercul/internal/domain/search" "tercul/internal/domain/search"
work_domain "tercul/internal/domain/work"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
) )
import "tercul/internal/app/authz" 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
}
// Application is a container for all the application-layer services. // Application is a container for all the application-layer services.
type Application struct { type Application struct {
Author *author.Service Author *author.Service
@ -41,22 +69,21 @@ type Application struct {
Analytics analytics.Service Analytics analytics.Service
} }
func NewApplication(repos *sql.Repositories, searchClient search.SearchClient, analyticsService analytics.Service) *Application { func NewApplication(deps Dependencies) *Application {
jwtManager := platform_auth.NewJWTManager() authzService := authz.NewService(deps.WorkRepo, deps.TranslationRepo)
authzService := authz.NewService(repos.Work, repos.Translation) authorService := author.NewService(deps.AuthorRepo)
authorService := author.NewService(repos.Author) bookService := book.NewService(deps.BookRepo, authzService)
bookService := book.NewService(repos.Book, authzService) bookmarkService := bookmark.NewService(deps.BookmarkRepo, deps.AnalyticsService)
bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService) categoryService := category.NewService(deps.CategoryRepo)
categoryService := category.NewService(repos.Category) collectionService := collection.NewService(deps.CollectionRepo)
collectionService := collection.NewService(repos.Collection) commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService)
commentService := comment.NewService(repos.Comment, authzService, analyticsService) likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService)
likeService := like.NewService(repos.Like, analyticsService) tagService := tag.NewService(deps.TagRepo)
tagService := tag.NewService(repos.Tag) translationService := translation.NewService(deps.TranslationRepo, authzService)
translationService := translation.NewService(repos.Translation, authzService) userService := user.NewService(deps.UserRepo, authzService)
userService := user.NewService(repos.User, authzService) localizationService := localization.NewService(deps.LocalizationRepo)
localizationService := localization.NewService(repos.Localization) authService := auth.NewService(deps.UserRepo, deps.JWTManager)
authService := auth.NewService(repos.User, jwtManager) workService := work.NewService(deps.WorkRepo, deps.SearchClient, authzService)
workService := work.NewService(repos.Work, searchClient, authzService)
return &Application{ return &Application{
Author: authorService, Author: authorService,
@ -73,6 +100,6 @@ func NewApplication(repos *sql.Repositories, searchClient search.SearchClient, a
Auth: authService, Auth: authService,
Authz: authzService, Authz: authzService,
Work: workService, Work: workService,
Analytics: analyticsService, Analytics: deps.AnalyticsService,
} }
} }

View File

@ -5,11 +5,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"tercul/internal/platform/config"
"tercul/internal/platform/log"
"time" "time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"tercul/internal/platform/config"
"tercul/internal/platform/log"
) )
// RedisCache implements the Cache interface using Redis // RedisCache implements the Cache interface using Redis
@ -37,12 +37,12 @@ func NewRedisCache(client *redis.Client, keyGenerator KeyGenerator, defaultExpir
} }
// NewDefaultRedisCache creates a new RedisCache with default settings // NewDefaultRedisCache creates a new RedisCache with default settings
func NewDefaultRedisCache() (*RedisCache, error) { func NewDefaultRedisCache(cfg *config.Config) (*RedisCache, error) {
// Create Redis client from config // Create Redis client from config
client := redis.NewClient(&redis.Options{ client := redis.NewClient(&redis.Options{
Addr: config.Cfg.RedisAddr, Addr: cfg.RedisAddr,
Password: config.Cfg.RedisPassword, Password: cfg.RedisPassword,
DB: config.Cfg.RedisDB, DB: cfg.RedisDB,
}) })
// Test connection // Test connection

View File

@ -1,161 +1,57 @@
package config package config
import ( import (
"fmt" "github.com/spf13/viper"
"log"
"os"
"strconv"
"time"
) )
// Config holds all configuration for the application // Config stores all configuration of the application.
type Config struct { type Config struct {
// Database configuration Environment string `mapstructure:"ENVIRONMENT"`
DBHost string ServerPort string `mapstructure:"SERVER_PORT"`
DBPort string DBHost string `mapstructure:"DB_HOST"`
DBUser string DBPort string `mapstructure:"DB_PORT"`
DBPassword string DBUser string `mapstructure:"DB_USER"`
DBName string DBPassword string `mapstructure:"DB_PASSWORD"`
DBSSLMode string DBName string `mapstructure:"DB_NAME"`
DBTimeZone string JWTSecret string `mapstructure:"JWT_SECRET"`
JWTExpiration int `mapstructure:"JWT_EXPIRATION_HOURS"`
// Weaviate configuration WeaviateHost string `mapstructure:"WEAVIATE_HOST"`
WeaviateScheme string WeaviateScheme string `mapstructure:"WEAVIATE_SCHEME"`
WeaviateHost string MigrationPath string `mapstructure:"MIGRATION_PATH"`
RedisAddr string `mapstructure:"REDIS_ADDR"`
// Redis configuration RedisPassword string `mapstructure:"REDIS_PASSWORD"`
RedisAddr string RedisDB int `mapstructure:"REDIS_DB"`
RedisPassword string SyncBatchSize int `mapstructure:"SYNC_BATCH_SIZE"`
RedisDB int RateLimit int `mapstructure:"RATE_LIMIT"`
RateLimitBurst int `mapstructure:"RATE_LIMIT_BURST"`
// Application configuration
Port string
ServerPort string
PlaygroundPort string
Environment string
LogLevel string
// Performance configuration
BatchSize int
PageSize int
RetryInterval time.Duration
MaxRetries int
// Security configuration
RateLimit int // Requests per second
RateLimitBurst int // Maximum burst size
JWTSecret string
JWTExpiration time.Duration
// NLP providers configuration
NLPUseLingua bool
NLPUseVADER bool
NLPUseTFIDF bool
// NLP cache configuration
NLPMemoryCacheCap int
NLPRedisCacheTTLSeconds int
} }
// Cfg is the global configuration instance // LoadConfig reads configuration from file or environment variables.
var Cfg Config func LoadConfig() (*Config, error) {
viper.SetDefault("ENVIRONMENT", "development")
viper.SetDefault("SERVER_PORT", ":8080")
viper.SetDefault("DB_HOST", "localhost")
viper.SetDefault("DB_PORT", "5432")
viper.SetDefault("DB_USER", "user")
viper.SetDefault("DB_PASSWORD", "password")
viper.SetDefault("DB_NAME", "tercul")
viper.SetDefault("JWT_SECRET", "secret")
viper.SetDefault("JWT_EXPIRATION_HOURS", 24)
viper.SetDefault("WEAVIATE_HOST", "localhost:8080")
viper.SetDefault("WEAVIATE_SCHEME", "http")
viper.SetDefault("MIGRATION_PATH", "internal/data/migrations")
viper.SetDefault("REDIS_ADDR", "localhost:6379")
viper.SetDefault("REDIS_PASSWORD", "")
viper.SetDefault("REDIS_DB", 0)
viper.SetDefault("SYNC_BATCH_SIZE", 100)
viper.SetDefault("RATE_LIMIT", 10)
viper.SetDefault("RATE_LIMIT_BURST", 100)
// LoadConfig loads configuration from environment variables viper.AutomaticEnv()
func LoadConfig() {
Cfg = Config{
// Database configuration
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "5432"),
DBUser: getEnv("DB_USER", "postgres"),
DBPassword: getEnv("DB_PASSWORD", "postgres"),
DBName: getEnv("DB_NAME", "tercul"),
DBSSLMode: getEnv("DB_SSLMODE", "disable"),
DBTimeZone: getEnv("DB_TIMEZONE", "UTC"),
// Weaviate configuration var config Config
WeaviateScheme: getEnv("WEAVIATE_SCHEME", "http"), if err := viper.Unmarshal(&config); err != nil {
WeaviateHost: getEnv("WEAVIATE_HOST", "localhost:8080"), return nil, err
// Redis configuration
RedisAddr: getEnv("REDIS_ADDR", "127.0.0.1:6379"),
RedisPassword: getEnv("REDIS_PASSWORD", ""),
RedisDB: getEnvAsInt("REDIS_DB", 0),
// Application configuration
Port: getEnv("PORT", "8080"),
ServerPort: getEnv("SERVER_PORT", "8080"),
PlaygroundPort: getEnv("PLAYGROUND_PORT", "8081"),
Environment: getEnv("ENVIRONMENT", "development"),
LogLevel: getEnv("LOG_LEVEL", "info"),
// Performance configuration
BatchSize: getEnvAsInt("BATCH_SIZE", 100),
PageSize: getEnvAsInt("PAGE_SIZE", 20),
RetryInterval: time.Duration(getEnvAsInt("RETRY_INTERVAL_SECONDS", 2)) * time.Second,
MaxRetries: getEnvAsInt("MAX_RETRIES", 3),
// Security configuration
RateLimit: getEnvAsInt("RATE_LIMIT", 10), // 10 requests per second by default
RateLimitBurst: getEnvAsInt("RATE_LIMIT_BURST", 50), // 50 burst requests by default
JWTSecret: getEnv("JWT_SECRET", ""),
JWTExpiration: time.Duration(getEnvAsInt("JWT_EXPIRATION_HOURS", 24)) * time.Hour,
// NLP providers configuration (enabled by default)
NLPUseLingua: getEnvAsBool("NLP_USE_LINGUA", true),
NLPUseVADER: getEnvAsBool("NLP_USE_VADER", true),
NLPUseTFIDF: getEnvAsBool("NLP_USE_TFIDF", true),
// NLP cache configuration
NLPMemoryCacheCap: getEnvAsInt("NLP_MEMORY_CACHE_CAP", 1024),
NLPRedisCacheTTLSeconds: getEnvAsInt("NLP_REDIS_CACHE_TTL_SECONDS", 86400),
}
log.Printf("Configuration loaded: Environment=%s, LogLevel=%s", Cfg.Environment, Cfg.LogLevel)
}
// GetDSN returns the database connection string
func (c *Config) GetDSN() string {
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName, c.DBSSLMode, c.DBTimeZone)
}
// Helper functions for environment variables
// getEnv gets an environment variable or returns a default value
func getEnv(key, defaultValue string) string {
value, exists := os.LookupEnv(key)
if !exists {
return defaultValue
}
return value
}
// getEnvAsInt gets an environment variable as an integer or returns a default value
func getEnvAsInt(key string, defaultValue int) int {
valueStr := getEnv(key, "")
if valueStr == "" {
return defaultValue
}
value, err := strconv.Atoi(valueStr)
if err != nil {
log.Printf("Warning: Invalid value for %s, using default: %v", key, err)
return defaultValue
}
return value
}
// getEnvAsBool gets an environment variable as a boolean or returns a default value
func getEnvAsBool(key string, defaultValue bool) bool {
valueStr := getEnv(key, "")
if valueStr == "" {
return defaultValue
}
switch valueStr {
case "1", "true", "TRUE", "True", "yes", "YES", "Yes", "on", "ON", "On":
return true
case "0", "false", "FALSE", "False", "no", "NO", "No", "off", "OFF", "Off":
return false
default:
return defaultValue
} }
return &config, nil
} }

View File

@ -3,24 +3,23 @@ package db
import ( import (
"fmt" "fmt"
"tercul/internal/observability" "tercul/internal/observability"
"tercul/internal/platform/config"
"tercul/internal/platform/log"
"time" "time"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
gormlogger "gorm.io/gorm/logger" gormlogger "gorm.io/gorm/logger"
"tercul/internal/platform/config"
"tercul/internal/platform/log"
) )
// DB is a global database connection instance // Connect establishes a connection to the database using the provided configuration.
var DB *gorm.DB // It returns the database connection and any error encountered.
func Connect(cfg *config.Config, metrics *observability.Metrics) (*gorm.DB, error) {
log.Info(fmt.Sprintf("Connecting to database: host=%s db=%s", cfg.DBHost, cfg.DBName))
// Connect establishes a connection to the database using configuration settings dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
// It returns the database connection and any error encountered cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort)
func Connect(metrics *observability.Metrics) (*gorm.DB, error) {
log.Info(fmt.Sprintf("Connecting to database: host=%s db=%s", config.Cfg.DBHost, config.Cfg.DBName))
dsn := config.Cfg.GetDSN()
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: gormlogger.Default.LogMode(gormlogger.Warn), Logger: gormlogger.Default.LogMode(gormlogger.Warn),
}) })
@ -33,9 +32,6 @@ func Connect(metrics *observability.Metrics) (*gorm.DB, error) {
return nil, fmt.Errorf("failed to register prometheus plugin: %w", err) return nil, fmt.Errorf("failed to register prometheus plugin: %w", err)
} }
// Set the global DB instance
DB = db
// Get the underlying SQL DB instance // Get the underlying SQL DB instance
sqlDB, err := db.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
@ -47,18 +43,18 @@ func Connect(metrics *observability.Metrics) (*gorm.DB, error) {
sqlDB.SetMaxIdleConns(5) // Idle connections sqlDB.SetMaxIdleConns(5) // Idle connections
sqlDB.SetConnMaxLifetime(30 * time.Minute) sqlDB.SetConnMaxLifetime(30 * time.Minute)
log.Info(fmt.Sprintf("Successfully connected to database: host=%s db=%s", config.Cfg.DBHost, config.Cfg.DBName)) log.Info(fmt.Sprintf("Successfully connected to database: host=%s db=%s", cfg.DBHost, cfg.DBName))
return db, nil return db, nil
} }
// Close closes the database connection // Close closes the database connection.
func Close() error { func Close(db *gorm.DB) error {
if DB == nil { if db == nil {
return nil return nil
} }
sqlDB, err := DB.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
return fmt.Errorf("failed to get SQL DB instance: %w", err) return fmt.Errorf("failed to get SQL DB instance: %w", err)
} }
@ -66,16 +62,12 @@ func Close() error {
return sqlDB.Close() return sqlDB.Close()
} }
// InitDB initializes the database connection and runs migrations // InitDB initializes the database connection.
// It returns the database connection and any error encountered func InitDB(cfg *config.Config, metrics *observability.Metrics) (*gorm.DB, error) {
func InitDB(metrics *observability.Metrics) (*gorm.DB, error) {
// Connect to the database // Connect to the database
db, err := Connect(metrics) db, err := Connect(cfg, metrics)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Migrations are now handled by a separate tool
return db, nil return db, nil
} }

View File

@ -163,7 +163,32 @@ func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
s.T().Fatalf("Failed to create sentiment provider: %v", err) s.T().Fatalf("Failed to create sentiment provider: %v", err)
} }
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
s.App = app.NewApplication(repos, searchClient, analyticsService) jwtManager := platform_auth.NewJWTManager()
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,
}
s.App = app.NewApplication(deps)
// Create a default admin user for tests // Create a default admin user for tests
adminUser := &domain.User{ adminUser := &domain.User{