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.)* - [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`. - **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`. - **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. - [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`.)*
- [ ] Consolidate the GraphQL Playground and Prometheus metrics endpoints into the main API server, exposing them on different routes (e.g., `/playground`, `/metrics`). - [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 ### EPIC: Comprehensive Documentation
@ -42,9 +42,9 @@ This document is the single source of truth for all outstanding development task
### EPIC: Core Architectural Refactoring ### EPIC: Core Architectural Refactoring
- [ ] **Refactor Dependency Injection:** The application's DI container in `internal/app/app.go` violates the Dependency Inversion Principle. - [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`.)*
- [ ] Refactor `NewApplication` to accept repository *interfaces* (e.g., `domain.WorkRepository`) instead of the concrete `*sql.Repositories`. - [x] 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] 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. - [ ] **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. - [ ] 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`). - [ ] **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/adapters/graphql"
"tercul/internal/app" "tercul/internal/app"
"tercul/internal/app/analytics" "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/localization"
appsearch "tercul/internal/app/search" 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" dbsql "tercul/internal/data/sql"
"tercul/internal/jobs/linguistics" "tercul/internal/jobs/linguistics"
"tercul/internal/observability" "tercul/internal/observability"
"tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"tercul/internal/platform/db" "tercul/internal/platform/db"
app_log "tercul/internal/platform/log" app_log "tercul/internal/platform/log"
"tercul/internal/platform/search" "tercul/internal/platform/search"
"time" "time"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/pressly/goose/v3" "github.com/pressly/goose/v3"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/weaviate/weaviate-go-client/v5/weaviate" "github.com/weaviate/weaviate-go-client/v5/weaviate"
@ -113,62 +126,61 @@ func main() {
} }
// Create platform components // Create platform components
jwtManager := auth.NewJWTManager(cfg) jwtManager := platform_auth.NewJWTManager(cfg)
// Create application services // Create application services
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
localizationService := localization.NewService(repos.Localization) localizationService := localization.NewService(repos.Localization)
searchService := appsearch.NewService(searchClient, localizationService) searchService := appsearch.NewService(searchClient, localizationService)
authzService := authz.NewService(repos.Work, repos.Translation)
// Create application dependencies authorService := author.NewService(repos.Author)
deps := app.Dependencies{ bookService := book.NewService(repos.Book, authzService)
WorkRepo: repos.Work, bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
UserRepo: repos.User, categoryService := category.NewService(repos.Category)
UserProfileRepo: repos.UserProfile, collectionService := collection.NewService(repos.Collection)
AuthorRepo: repos.Author, commentService := comment.NewService(repos.Comment, authzService, analyticsService)
TranslationRepo: repos.Translation, contributionCommands := contribution.NewCommands(repos.Contribution, authzService)
CommentRepo: repos.Comment, contributionService := contribution.NewService(contributionCommands)
LikeRepo: repos.Like, likeService := like.NewService(repos.Like, analyticsService)
BookmarkRepo: repos.Bookmark, tagService := tag.NewService(repos.Tag)
CollectionRepo: repos.Collection, translationService := translation.NewService(repos.Translation, authzService)
TagRepo: repos.Tag, userService := user.NewService(repos.User, authzService, repos.UserProfile)
CategoryRepo: repos.Category, authService := auth.NewService(repos.User, jwtManager)
BookRepo: repos.Book, workService := work.NewService(repos.Work, searchClient, authzService)
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 // Create application
application := app.NewApplication(deps) application := app.NewApplication(
application.Search = searchService // Manually set the search service authorService,
bookService,
bookmarkService,
categoryService,
collectionService,
commentService,
contributionService,
likeService,
tagService,
translationService,
userService,
localizationService,
authService,
authzService,
workService,
searchService,
analyticsService,
)
// Create GraphQL server // Create GraphQL server
resolver := &graphql.Resolver{ resolver := &graphql.Resolver{
App: application, App: application,
} }
// Create the main API handler with all middleware. // Create the consolidated API server with all routes.
apiHandler := NewServerWithAuth(resolver, jwtManager, metrics, obsLogger) apiHandler := NewAPIServer(resolver, jwtManager, metrics, obsLogger, reg)
// Create the main ServeMux and register all handlers. // Create the main HTTP server.
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.
mainServer := &http.Server{ mainServer := &http.Server{
Addr: cfg.ServerPort, Addr: cfg.ServerPort,
Handler: mux, Handler: apiHandler,
} }
app_log.Info(fmt.Sprintf("API server listening on port %s", cfg.ServerPort)) app_log.Info(fmt.Sprintf("API server listening on port %s", cfg.ServerPort))

View File

@ -7,28 +7,42 @@ import (
"tercul/internal/platform/auth" "tercul/internal/platform/auth"
"github.com/99designs/gqlgen/graphql/handler" "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 // NewAPIServer creates a new http.ServeMux and configures it with all the API routes,
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager, metrics *observability.Metrics, logger *observability.Logger) http.Handler { // 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 := graphql.Config{Resolvers: resolver}
c.Directives.Binding = graphql.Binding c.Directives.Binding = graphql.Binding
// Create the server with the custom error presenter // Create the core GraphQL handler
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c)) graphqlHandler := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
srv.SetErrorPresenter(graphql.NewErrorPresenter()) graphqlHandler.SetErrorPresenter(graphql.NewErrorPresenter())
// Create a middleware chain. The order is important. // Create the middleware chain for the GraphQL endpoint.
// Middlewares are applied from bottom to top, so the last one added is the first to run. // Middlewares are applied from bottom to top.
var chain http.Handler var chain http.Handler
chain = srv chain = graphqlHandler
chain = metrics.PrometheusMiddleware(chain) chain = metrics.PrometheusMiddleware(chain)
// LoggingMiddleware needs to run after auth and tracing to get all context. chain = observability.LoggingMiddleware(logger)(chain) // Must run after auth and tracing
chain = observability.LoggingMiddleware(logger)(chain)
chain = auth.GraphQLAuthMiddleware(jwtManager)(chain) chain = auth.GraphQLAuthMiddleware(jwtManager)(chain)
chain = observability.TracingMiddleware(chain) chain = observability.TracingMiddleware(chain)
chain = observability.RequestIDMiddleware(chain) chain = observability.RequestIDMiddleware(chain)
// Return the handler chain directly. The caller is responsible for routing. // Create a new ServeMux and register all handlers
return chain 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/translation"
"tercul/internal/app/user" "tercul/internal/app/user"
"tercul/internal/app/work" "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. // Application is a container for all the application-layer services.
type Application struct { type Application struct {
Author *author.Service Author *author.Service
@ -71,42 +41,43 @@ type Application struct {
Analytics analytics.Service Analytics analytics.Service
} }
func NewApplication(deps Dependencies) *Application { // NewApplication creates a new Application container from pre-built services.
authzService := authz.NewService(deps.WorkRepo, deps.TranslationRepo) func NewApplication(
authorService := author.NewService(deps.AuthorRepo) authorSvc *author.Service,
bookService := book.NewService(deps.BookRepo, authzService) bookSvc *book.Service,
bookmarkService := bookmark.NewService(deps.BookmarkRepo, deps.AnalyticsService) bookmarkSvc *bookmark.Service,
categoryService := category.NewService(deps.CategoryRepo) categorySvc *category.Service,
collectionService := collection.NewService(deps.CollectionRepo) collectionSvc *collection.Service,
commentService := comment.NewService(deps.CommentRepo, authzService, deps.AnalyticsService) commentSvc *comment.Service,
contributionCommands := contribution.NewCommands(deps.ContributionRepo, authzService) contributionSvc *contribution.Service,
contributionService := contribution.NewService(contributionCommands) likeSvc *like.Service,
likeService := like.NewService(deps.LikeRepo, deps.AnalyticsService) tagSvc *tag.Service,
tagService := tag.NewService(deps.TagRepo) translationSvc *translation.Service,
translationService := translation.NewService(deps.TranslationRepo, authzService) userSvc *user.Service,
userService := user.NewService(deps.UserRepo, authzService, deps.UserProfileRepo) localizationSvc *localization.Service,
localizationService := localization.NewService(deps.LocalizationRepo) authSvc *auth.Service,
authService := auth.NewService(deps.UserRepo, deps.JWTManager) authzSvc *authz.Service,
workService := work.NewService(deps.WorkRepo, deps.SearchClient, authzService) workSvc *work.Service,
searchService := appsearch.NewService(deps.SearchClient, localizationService) searchSvc appsearch.Service,
analyticsSvc analytics.Service,
) *Application {
return &Application{ return &Application{
Author: authorService, Author: authorSvc,
Book: bookService, Book: bookSvc,
Bookmark: bookmarkService, Bookmark: bookmarkSvc,
Category: categoryService, Category: categorySvc,
Collection: collectionService, Collection: collectionSvc,
Comment: commentService, Comment: commentSvc,
Contribution: contributionService, Contribution: contributionSvc,
Like: likeService, Like: likeSvc,
Tag: tagService, Tag: tagSvc,
Translation: translationService, Translation: translationSvc,
User: userService, User: userSvc,
Localization: localizationService, Localization: localizationSvc,
Auth: authService, Auth: authSvc,
Authz: authzService, Authz: authzSvc,
Work: workService, Work: workSvc,
Search: searchService, Search: searchSvc,
Analytics: deps.AnalyticsService, Analytics: analyticsSvc,
} }
} }

View File

@ -7,7 +7,22 @@ import (
"path/filepath" "path/filepath"
"tercul/internal/app" "tercul/internal/app"
"tercul/internal/app/analytics" "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/translation"
"tercul/internal/app/user"
"tercul/internal/app/work"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/domain/search" "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) analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
jwtManager := platform_auth.NewJWTManager(cfg) jwtManager := platform_auth.NewJWTManager(cfg)
deps := app.Dependencies{ authzService := authz.NewService(repos.Work, repos.Translation)
WorkRepo: repos.Work, authorService := author.NewService(repos.Author)
UserRepo: repos.User, bookService := book.NewService(repos.Book, authzService)
AuthorRepo: repos.Author, bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
TranslationRepo: repos.Translation, categoryService := category.NewService(repos.Category)
CommentRepo: repos.Comment, collectionService := collection.NewService(repos.Collection)
LikeRepo: repos.Like, commentService := comment.NewService(repos.Comment, authzService, analyticsService)
BookmarkRepo: repos.Bookmark, contributionCommands := contribution.NewCommands(repos.Contribution, authzService)
CollectionRepo: repos.Collection, contributionService := contribution.NewService(contributionCommands)
TagRepo: repos.Tag, likeService := like.NewService(repos.Like, analyticsService)
CategoryRepo: repos.Category, tagService := tag.NewService(repos.Tag)
BookRepo: repos.Book, translationService := translation.NewService(repos.Translation, authzService)
PublisherRepo: repos.Publisher, userService := user.NewService(repos.User, authzService, repos.UserProfile)
SourceRepo: repos.Source, localizationService := localization.NewService(repos.Localization)
CopyrightRepo: repos.Copyright, authService := app_auth.NewService(repos.User, jwtManager)
MonetizationRepo: repos.Monetization, workService := work.NewService(repos.Work, searchClient, authzService)
ContributionRepo: repos.Contribution, searchService := app_search.NewService(searchClient, localizationService)
AnalyticsRepo: repos.Analytics,
AuthRepo: repos.Auth, s.App = app.NewApplication(
LocalizationRepo: repos.Localization, authorService,
SearchClient: searchClient, bookService,
AnalyticsService: analyticsService, bookmarkService,
JWTManager: jwtManager, categoryService,
} collectionService,
s.App = app.NewApplication(deps) commentService,
contributionService,
likeService,
tagService,
translationService,
userService,
localizationService,
authService,
authzService,
workService,
searchService,
analyticsService,
)
// Create a default admin user for tests // Create a default admin user for tests
adminUser := &domain.User{ adminUser := &domain.User{