Files
spsays-bff/internal/server/server.go
T
2026-05-09 23:19:19 +05:30

332 lines
11 KiB
Go

package server
import (
"net/http"
"github.com/go-chi/chi/v5"
chiMiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/prabhupadasays/spsays-bff/internal/config"
"github.com/prabhupadasays/spsays-bff/internal/handler"
"github.com/prabhupadasays/spsays-bff/internal/middleware"
"github.com/prabhupadasays/spsays-bff/internal/repository"
"go.uber.org/zap"
)
// Server holds all dependencies for the HTTP server.
type Server struct {
cfg *config.Config
logger *zap.Logger
router chi.Router
mongo *repository.MongoDB
// Handlers
bookH *handler.BookHandler
letterH *handler.LetterHandler
essayH *handler.EssayHandler
glossaryH *handler.GlossaryHandler
discipleH *handler.DiscipleHandler
journalH *handler.JournalHandler
magazineH *handler.MagazineHandler
otherDocH *handler.OtherDocumentHandler
transcriptionH *handler.TranscriptionHandler
}
// New creates a new Server with all routes registered.
func New(cfg *config.Config, logger *zap.Logger) *Server {
mongoDB, err := repository.NewMongoDB(cfg.MongoURI, cfg.MongoDB, logger)
if err != nil {
logger.Fatal("failed to connect to MongoDB", zap.Error(err))
}
// Initialize repositories
bookRepo := repository.NewBookRepo(mongoDB)
letterRepo := repository.NewLetterRepo(mongoDB)
essayRepo := repository.NewEssayRepo(mongoDB)
glossaryRepo := repository.NewGlossaryRepo(mongoDB)
discipleRepo := repository.NewDiscipleRepo(mongoDB)
journalRepo := repository.NewJournalRepo(mongoDB)
magazineRepo := repository.NewMagazineRepo(mongoDB)
otherDocRepo := repository.NewOtherDocumentRepo(mongoDB)
transcriptionRepo := repository.NewTranscriptionRepo(mongoDB)
s := &Server{
cfg: cfg,
logger: logger,
mongo: mongoDB,
// Initialize handlers
bookH: handler.NewBookHandler(bookRepo),
letterH: handler.NewLetterHandler(letterRepo),
essayH: handler.NewEssayHandler(essayRepo),
glossaryH: handler.NewGlossaryHandler(glossaryRepo),
discipleH: handler.NewDiscipleHandler(discipleRepo),
journalH: handler.NewJournalHandler(journalRepo),
magazineH: handler.NewMagazineHandler(magazineRepo),
otherDocH: handler.NewOtherDocumentHandler(otherDocRepo),
transcriptionH: handler.NewTranscriptionHandler(transcriptionRepo),
}
s.setupRouter()
return s
}
// Router returns the chi router.
func (s *Server) Router() http.Handler {
return s.router
}
// Close shuts down all connections gracefully.
func (s *Server) Close() {
if err := s.mongo.Close(); err != nil {
s.logger.Error("failed to close MongoDB connection", zap.Error(err))
}
// TODO: close PostgreSQL, Redis, etc. connections
s.logger.Info("all connections closed")
}
// injectBookCode is a middleware that sets "code" in chi's URL params
// for explicit sub-routes like /books/sb and /books/cc where {code}
// isn't a URL param but a fixed path prefix.
func injectBookCode(code string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
rctx.URLParams.Add("code", code)
next.ServeHTTP(w, r)
})
}
}
func (s *Server) setupRouter() {
r := chi.NewRouter()
// Global middleware
r.Use(chiMiddleware.RequestID)
r.Use(chiMiddleware.RealIP)
r.Use(middleware.Logger(s.logger))
r.Use(chiMiddleware.Recoverer)
r.Use(chiMiddleware.Compress(5))
// CORS
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{s.cfg.FrontendURL},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Request-ID"},
ExposedHeaders: []string{"X-Request-ID"},
AllowCredentials: true,
MaxAge: 300,
}))
// Health check
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
// API v1
r.Route("/api/v1", func(r chi.Router) {
// Books
r.Route("/books", func(r chi.Router) {
r.Get("/", s.bookH.ListBooks)
// --- Verse books with divisions ---
// SB — canto divisions
r.Route("/sb", func(r chi.Router) {
r.Use(injectBookCode("sb"))
r.Get("/", s.bookH.GetBook)
r.Get("/{division}", s.bookH.GetDivisionInfo)
r.Get("/{division}/{chapter}", s.bookH.GetDivisionChapterVerseIndex)
r.Get("/{division}/{chapter}/verses", s.bookH.GetDivisionChapterVerses)
r.Get("/{division}/{chapter}/{verse}", s.bookH.GetDivisionVerse)
})
// CC — līlā divisions
r.Route("/cc", func(r chi.Router) {
r.Use(injectBookCode("cc"))
r.Get("/", s.bookH.GetBook)
r.Get("/{division}", s.bookH.GetDivisionInfo)
r.Get("/{division}/{chapter}", s.bookH.GetDivisionChapterVerseIndex)
r.Get("/{division}/{chapter}/verses", s.bookH.GetDivisionChapterVerses)
r.Get("/{division}/{chapter}/{verse}", s.bookH.GetDivisionVerse)
})
// --- Verse books without divisions ---
// BG, SB1962: pure verse books
for _, code := range []string{"bg", "sb1962"} {
code := code // capture for closure
r.Route("/"+code, func(r chi.Router) {
r.Use(injectBookCode(code))
r.Get("/", s.bookH.GetBook)
r.Get("/{chapter}", s.bookH.GetChapterVerseIndex)
r.Get("/{chapter}/verses", s.bookH.GetChapterVerses)
r.Get("/{chapter}/{verse}", s.bookH.GetVerse)
})
}
// --- Verse + prose intro books (no division) ---
// BS, MM, NBS: have both verses and prose intro segments
for _, code := range []string{"bs", "mm", "nbs"} {
code := code
r.Route("/"+code, func(r chi.Router) {
r.Use(injectBookCode(code))
r.Get("/", s.bookH.GetBook)
r.Get("/chapters", s.bookH.ListProseChapters)
r.Get("/chapters/{slug}", s.bookH.GetProseChapter)
r.Get("/{chapter}", s.bookH.GetChapterVerseIndex)
r.Get("/{chapter}/verses", s.bookH.GetChapterVerses)
r.Get("/{chapter}/{verse}", s.bookH.GetVerse)
})
}
// --- Verse + prose intro books with special slugs ---
// ISO, NOI: mantras/texts accessed by slug, plus prose intros
for _, code := range []string{"iso", "noi"} {
code := code
r.Route("/"+code, func(r chi.Router) {
r.Use(injectBookCode(code))
r.Get("/", s.bookH.GetBook)
r.Get("/chapters", s.bookH.ListProseChapters)
r.Get("/chapters/{slug}", s.bookH.GetProseChapter)
r.Get("/{chapter}", s.bookH.GetChapterVerseIndex)
r.Get("/{chapter}/verses", s.bookH.GetChapterVerses)
r.Get("/{chapter}/{verse}", s.bookH.GetVerse)
})
}
// --- Volume-based prose book ---
// KB: 2 volumes, chapters within each volume
r.Route("/kb", func(r chi.Router) {
r.Use(injectBookCode("kb"))
r.Get("/", s.bookH.GetBook)
r.Get("/{volume}", s.bookH.ListVolumeChapters)
r.Get("/{volume}/{slug}", s.bookH.GetVolumeChapter)
})
// --- Simple prose books ---
// BBD, BK, CBHC, DS, EJ, EK, GG, HKH, LCL, LOB, MG, MOG, NOD,
// OCHKM, OWK, PK, POY, PQPA, RV, SSR, SVA, TLC, TLK, TQK, TTP, VA
for _, code := range []string{
"bbd", "bk", "cbhc", "ds", "ej", "ek", "gg", "hkh",
"lcl", "lob", "mg", "mog", "nod", "ochkm", "owk", "pk",
"poy", "pqpa", "rv", "ssr", "sva", "tlc", "tlk", "tqk",
"ttp", "va",
} {
code := code
r.Route("/"+code, func(r chi.Router) {
r.Use(injectBookCode(code))
r.Get("/", s.bookH.GetBook)
r.Get("/{slug}", s.bookH.GetProseChapter)
r.Get("/chapters", s.bookH.ListProseChapters)
r.Get("/chapters/{slug}", s.bookH.GetProseChapter)
})
}
})
// Letters
r.Route("/letters", func(r chi.Router) {
r.Get("/", s.letterH.Count)
r.Get("/years", s.letterH.GetYears)
r.Get("/recipients", s.letterH.GetRecipients)
r.Get("/locations", s.letterH.GetLocations)
r.Get("/year/{year}", s.letterH.GetByYear)
r.Get("/recipient/{name}", s.letterH.GetByRecipient)
r.Get("/location/{name}", s.letterH.GetByLocation)
r.Get("/{slug}", s.letterH.GetBySlug)
})
// Essays
r.Route("/essays", func(r chi.Router) {
r.Get("/", s.essayH.List)
r.Get("/{slug}", s.essayH.GetBySlug)
})
// Glossary
r.Route("/glossary", func(r chi.Router) {
r.Get("/", s.glossaryH.List)
r.Get("/letters", s.glossaryH.GetLetterCounts)
r.Get("/letter/{letter}", s.glossaryH.GetByLetter)
r.Get("/letter/{letter}/terms", s.glossaryH.GetTermsByLetter)
r.Get("/term/{term}", s.glossaryH.GetByTerm)
})
// Disciples
r.Route("/disciples", func(r chi.Router) {
r.Get("/", s.discipleH.List)
r.Get("/count", s.discipleH.Count)
r.Get("/letters", s.discipleH.LetterCounts)
r.Get("/suffixes", s.discipleH.SuffixCounts)
r.Get("/search", s.discipleH.Search)
})
// Journals
r.Route("/journals", func(r chi.Router) {
r.Get("/", s.journalH.ListJournals)
r.Get("/{book}", s.journalH.GetJournal)
r.Get("/{book}/chapters", s.journalH.ListChapters)
r.Get("/{book}/chapters/{slug}", s.journalH.GetChapter)
})
// Magazines
r.Route("/magazines", func(r chi.Router) {
r.Get("/", s.magazineH.ListMagazines)
r.Get("/{book}", s.magazineH.GetMagazine)
r.Get("/{book}/issues", s.magazineH.ListIssues)
r.Get("/{book}/issues/{slug}", s.magazineH.GetIssue)
})
// Other Documents
r.Route("/other-documents", func(r chi.Router) {
r.Get("/", s.otherDocH.ListDocuments)
r.Get("/{book}", s.otherDocH.GetDocument)
r.Get("/{book}/chapters", s.otherDocH.ListChapters)
r.Get("/{book}/chapters/{slug}", s.otherDocH.GetChapter)
})
// Transcriptions
r.Route("/transcriptions", func(r chi.Router) {
r.Get("/", s.transcriptionH.Count)
r.Get("/years", s.transcriptionH.GetYears)
r.Get("/places", s.transcriptionH.GetPlaces)
r.Get("/types", s.transcriptionH.GetTypeDistribution)
r.Get("/year/{year}", s.transcriptionH.GetByYear)
r.Get("/place/{place}", s.transcriptionH.GetByPlace)
r.Get("/type/{type}", s.transcriptionH.GetByType)
r.Get("/adjacent/{slug}", s.transcriptionH.GetAdjacent)
r.Get("/{slug}", s.transcriptionH.GetBySlug)
})
// Search (Meilisearch)
r.Route("/search", func(r chi.Router) {
// TODO: register search handlers
})
// User routes (PostgreSQL)
r.Route("/auth", func(r chi.Router) {
// TODO: register auth handlers (login, register, refresh)
})
r.Route("/users", func(r chi.Router) {
// TODO: register user handlers
})
r.Route("/collections", func(r chi.Router) {
// TODO: register collection/bookmark handlers
})
r.Route("/mentions", func(r chi.Router) {
// TODO: register mention handlers
})
// URL Shortener
r.Route("/s", func(r chi.Router) {
// TODO: register URL shortener handlers
})
// Push Notifications
r.Route("/notifications", func(r chi.Router) {
// TODO: register push notification handlers
})
})
s.router = r
}