feat: add books APIs
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user