mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 00:31:35 +00:00
Merge pull request #20 from SamyRai/refactor-api-server-setup
refactor(api): centralize server setup in NewAPIServer
This commit is contained in:
commit
cacd621139
10
TASKS.md
10
TASKS.md
@ -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`).
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user