Merge pull request #20 from SamyRai/refactor-api-server-setup

refactor(api): centralize server setup in NewAPIServer
This commit is contained in:
Damir Mukimov 2025-10-08 18:29:09 +02:00 committed by GitHub
commit cacd621139
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 174 additions and 150 deletions

View File

@ -19,8 +19,8 @@ This document is the single source of truth for all outstanding development task
- [x] **Implement All Unimplemented Resolvers:** The GraphQL API is critically incomplete. All of the following `panic`ing resolvers must be implemented. *(Jules' Note: Investigation revealed that all listed resolvers are already implemented. This task is complete.)*
- **Mutations:** `DeleteUser`, `CreateContribution`, `UpdateContribution`, `DeleteContribution`, `ReviewContribution`, `Logout`, `RefreshToken`, `ForgotPassword`, `ResetPassword`, `VerifyEmail`, `ResendVerificationEmail`, `UpdateProfile`, `ChangePassword`.
- **Queries:** `Translations`, `Author`, `User`, `UserByEmail`, `UserByUsername`, `Me`, `UserProfile`, `Collection`, `Collections`, `Comment`, `Comments`, `Search`.
- [ ] **Refactor API Server Setup:** The API server startup in `cmd/api/main.go` is unnecessarily complex.
- [ ] Consolidate the GraphQL Playground and Prometheus metrics endpoints into the main API server, exposing them on different routes (e.g., `/playground`, `/metrics`).
- [x] **Refactor API Server Setup:** The API server startup in `cmd/api/main.go` is unnecessarily complex. *(Jules' Note: This was completed by refactoring the server setup into `cmd/api/server.go`.)*
- [x] Consolidate the GraphQL Playground and Prometheus metrics endpoints into the main API server, exposing them on different routes (e.g., `/playground`, `/metrics`).
### EPIC: Comprehensive Documentation
@ -42,9 +42,9 @@ This document is the single source of truth for all outstanding development task
### EPIC: Core Architectural Refactoring
- [ ] **Refactor Dependency Injection:** The application's DI container in `internal/app/app.go` violates the Dependency Inversion Principle.
- [ ] Refactor `NewApplication` to accept repository *interfaces* (e.g., `domain.WorkRepository`) instead of the concrete `*sql.Repositories`.
- [ ] Move the instantiation of platform components (e.g., `JWTManager`) out of `NewApplication` and into `cmd/api/main.go`, passing them in as dependencies.
- [x] **Refactor Dependency Injection:** The application's DI container in `internal/app/app.go` violates the Dependency Inversion Principle. *(Jules' Note: The composition root has been moved to `cmd/api/main.go`.)*
- [x] Refactor `NewApplication` to accept repository *interfaces* (e.g., `domain.WorkRepository`) instead of the concrete `*sql.Repositories`.
- [x] Move the instantiation of platform components (e.g., `JWTManager`) out of `NewApplication` and into `cmd/api/main.go`, passing them in as dependencies.
- [ ] **Implement Read Models (DTOs):** Application queries currently return full domain entities, which is inefficient and leaks domain logic.
- [ ] Refactor application queries (e.g., in `internal/app/work/queries.go`) to return specialized read models (DTOs) tailored for the API.
- [ ] **Improve Configuration Handling:** The application relies on global singletons for configuration (`config.Cfg`).

View File

@ -11,19 +11,32 @@ import (
"tercul/internal/adapters/graphql"
"tercul/internal/app"
"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"
appsearch "tercul/internal/app/search"
"tercul/internal/app/tag"
"tercul/internal/app/translation"
"tercul/internal/app/user"
"tercul/internal/app/work"
dbsql "tercul/internal/data/sql"
"tercul/internal/jobs/linguistics"
"tercul/internal/observability"
"tercul/internal/platform/auth"
platform_auth "tercul/internal/platform/auth"
"tercul/internal/platform/config"
"tercul/internal/platform/db"
app_log "tercul/internal/platform/log"
"tercul/internal/platform/search"
"time"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/pressly/goose/v3"
"github.com/prometheus/client_golang/prometheus"
"github.com/weaviate/weaviate-go-client/v5/weaviate"
@ -113,62 +126,61 @@ func main() {
}
// Create platform components
jwtManager := auth.NewJWTManager(cfg)
jwtManager := platform_auth.NewJWTManager(cfg)
// Create application services
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
localizationService := localization.NewService(repos.Localization)
searchService := appsearch.NewService(searchClient, localizationService)
// Create application dependencies
deps := app.Dependencies{
WorkRepo: repos.Work,
UserRepo: repos.User,
UserProfileRepo: repos.UserProfile,
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,
}
authzService := authz.NewService(repos.Work, repos.Translation)
authorService := author.NewService(repos.Author)
bookService := book.NewService(repos.Book, authzService)
bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
categoryService := category.NewService(repos.Category)
collectionService := collection.NewService(repos.Collection)
commentService := comment.NewService(repos.Comment, authzService, analyticsService)
contributionCommands := contribution.NewCommands(repos.Contribution, authzService)
contributionService := contribution.NewService(contributionCommands)
likeService := like.NewService(repos.Like, analyticsService)
tagService := tag.NewService(repos.Tag)
translationService := translation.NewService(repos.Translation, authzService)
userService := user.NewService(repos.User, authzService, repos.UserProfile)
authService := auth.NewService(repos.User, jwtManager)
workService := work.NewService(repos.Work, searchClient, authzService)
// Create application
application := app.NewApplication(deps)
application.Search = searchService // Manually set the search service
application := app.NewApplication(
authorService,
bookService,
bookmarkService,
categoryService,
collectionService,
commentService,
contributionService,
likeService,
tagService,
translationService,
userService,
localizationService,
authService,
authzService,
workService,
searchService,
analyticsService,
)
// Create GraphQL server
resolver := &graphql.Resolver{
App: application,
}
// Create the main API handler with all middleware.
apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger)
// Create the consolidated API server with all routes.
apiHandler := NewAPIServer(resolver, jwtManager, metrics, obsLogger, reg)
// Create the main ServeMux and register all handlers.
mux := http.NewServeMux()
mux.Handle("/query", apiHandler)
mux.Handle("/playground", playground.Handler("GraphQL Playground", "/query"))
mux.Handle("/metrics", observability.PrometheusHandler(reg))
// Create a single HTTP server with the main mux.
// Create the main HTTP server.
mainServer := &http.Server{
Addr: cfg.ServerPort,
Handler: mux,
Handler: apiHandler,
}
app_log.Info(fmt.Sprintf("API server listening on port %s", cfg.ServerPort))

View File

@ -7,28 +7,42 @@ import (
"tercul/internal/platform/auth"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/prometheus/client_golang/prometheus"
)
// 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 {
// NewAPIServer creates a new http.ServeMux and configures it with all the API routes,
// including the GraphQL endpoint, GraphQL Playground, and Prometheus metrics.
func NewAPIServer(
resolver *graphql.Resolver,
jwtManager *auth.JWTManager,
metrics *observability.Metrics,
logger *observability.Logger,
reg *prometheus.Registry,
) *http.ServeMux {
// Configure the GraphQL server
c := graphql.Config{Resolvers: resolver}
c.Directives.Binding = graphql.Binding
// Create the server with the custom error presenter
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
srv.SetErrorPresenter(graphql.NewErrorPresenter())
// Create the core GraphQL handler
graphqlHandler := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
graphqlHandler.SetErrorPresenter(graphql.NewErrorPresenter())
// Create a middleware chain. The order is important.
// Middlewares are applied from bottom to top, so the last one added is the first to run.
// Create the middleware chain for the GraphQL endpoint.
// Middlewares are applied from bottom to top.
var chain http.Handler
chain = srv
chain = graphqlHandler
chain = metrics.PrometheusMiddleware(chain)
// LoggingMiddleware needs to run after auth and tracing to get all context.
chain = observability.LoggingMiddleware(logger)(chain)
chain = observability.LoggingMiddleware(logger)(chain) // Must run after auth and tracing
chain = auth.GraphQLAuthMiddleware(jwtManager)(chain)
chain = observability.TracingMiddleware(chain)
chain = observability.RequestIDMiddleware(chain)
// Return the handler chain directly. The caller is responsible for routing.
return chain
// Create a new ServeMux and register all handlers
mux := http.NewServeMux()
mux.Handle("/query", chain)
mux.Handle("/playground", playground.Handler("GraphQL Playground", "/query"))
mux.Handle("/metrics", observability.PrometheusHandler(reg))
return mux
}

View File

@ -18,38 +18,8 @@ import (
"tercul/internal/app/translation"
"tercul/internal/app/user"
"tercul/internal/app/work"
"tercul/internal/domain"
domainsearch "tercul/internal/domain/search"
platform_auth "tercul/internal/platform/auth"
)
// Dependencies holds all external dependencies for the application.
type Dependencies struct {
WorkRepo 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
UserProfileRepo domain.UserProfileRepository
AnalyticsRepo analytics.Repository
AuthRepo domain.AuthRepository
LocalizationRepo domain.LocalizationRepository
SearchClient domainsearch.SearchClient
AnalyticsService analytics.Service
JWTManager platform_auth.JWTManagement
}
// Application is a container for all the application-layer services.
type Application struct {
Author *author.Service
@ -71,42 +41,43 @@ type Application struct {
Analytics analytics.Service
}
func NewApplication(deps Dependencies) *Application {
authzService := authz.NewService(deps.WorkRepo, deps.TranslationRepo)
authorService := author.NewService(deps.AuthorRepo)
bookService := book.NewService(deps.BookRepo, authzService)
bookmarkService := bookmark.NewService(deps.BookmarkRepo, deps.AnalyticsService)
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)
userService := user.NewService(deps.UserRepo, authzService, deps.UserProfileRepo)
localizationService := localization.NewService(deps.LocalizationRepo)
authService := auth.NewService(deps.UserRepo, deps.JWTManager)
workService := work.NewService(deps.WorkRepo, deps.SearchClient, authzService)
searchService := appsearch.NewService(deps.SearchClient, localizationService)
// NewApplication creates a new Application container from pre-built services.
func NewApplication(
authorSvc *author.Service,
bookSvc *book.Service,
bookmarkSvc *bookmark.Service,
categorySvc *category.Service,
collectionSvc *collection.Service,
commentSvc *comment.Service,
contributionSvc *contribution.Service,
likeSvc *like.Service,
tagSvc *tag.Service,
translationSvc *translation.Service,
userSvc *user.Service,
localizationSvc *localization.Service,
authSvc *auth.Service,
authzSvc *authz.Service,
workSvc *work.Service,
searchSvc appsearch.Service,
analyticsSvc analytics.Service,
) *Application {
return &Application{
Author: authorService,
Book: bookService,
Bookmark: bookmarkService,
Category: categoryService,
Collection: collectionService,
Comment: commentService,
Contribution: contributionService,
Like: likeService,
Tag: tagService,
Translation: translationService,
User: userService,
Localization: localizationService,
Auth: authService,
Authz: authzService,
Work: workService,
Search: searchService,
Analytics: deps.AnalyticsService,
Author: authorSvc,
Book: bookSvc,
Bookmark: bookmarkSvc,
Category: categorySvc,
Collection: collectionSvc,
Comment: commentSvc,
Contribution: contributionSvc,
Like: likeSvc,
Tag: tagSvc,
Translation: translationSvc,
User: userSvc,
Localization: localizationSvc,
Auth: authSvc,
Authz: authzSvc,
Work: workSvc,
Search: searchSvc,
Analytics: analyticsSvc,
}
}

View File

@ -7,7 +7,22 @@ import (
"path/filepath"
"tercul/internal/app"
"tercul/internal/app/analytics"
app_auth "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"
app_search "tercul/internal/app/search"
"tercul/internal/app/tag"
"tercul/internal/app/translation"
"tercul/internal/app/user"
"tercul/internal/app/work"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/domain/search"
@ -122,31 +137,43 @@ func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
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,
ContributionRepo: repos.Contribution,
AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth,
LocalizationRepo: repos.Localization,
SearchClient: searchClient,
AnalyticsService: analyticsService,
JWTManager: jwtManager,
}
s.App = app.NewApplication(deps)
authzService := authz.NewService(repos.Work, repos.Translation)
authorService := author.NewService(repos.Author)
bookService := book.NewService(repos.Book, authzService)
bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
categoryService := category.NewService(repos.Category)
collectionService := collection.NewService(repos.Collection)
commentService := comment.NewService(repos.Comment, authzService, analyticsService)
contributionCommands := contribution.NewCommands(repos.Contribution, authzService)
contributionService := contribution.NewService(contributionCommands)
likeService := like.NewService(repos.Like, analyticsService)
tagService := tag.NewService(repos.Tag)
translationService := translation.NewService(repos.Translation, authzService)
userService := user.NewService(repos.User, authzService, repos.UserProfile)
localizationService := localization.NewService(repos.Localization)
authService := app_auth.NewService(repos.User, jwtManager)
workService := work.NewService(repos.Work, searchClient, authzService)
searchService := app_search.NewService(searchClient, localizationService)
s.App = app.NewApplication(
authorService,
bookService,
bookmarkService,
categoryService,
collectionService,
commentService,
contributionService,
likeService,
tagService,
translationService,
userService,
localizationService,
authService,
authzService,
workService,
searchService,
analyticsService,
)
// Create a default admin user for tests
adminUser := &domain.User{