- Auto-crop thumbnails on image upload (Go stdlib, no external deps): find non-background bounding box, pad 5px, scale to ≤300px, encode JPEG - Add thumbnail_data BYTEA column to fruit_images and publication_fruit_images - New GET /api/v1/fruits/:id/images/:imageId/thumbnail endpoint (fallback to full data) - New GET /api/v1/publications/:id/fruit-images/:imgId/thumbnail endpoint - New POST /api/v1/admin/backfill-thumbnails to retroactively generate thumbnails - ListFruits response now includes synonyms and thumbnail_url per fruit - Replace FruitList table with responsive FruitCard grid (thumbnail + name + OSDB ID + type + synonyms) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
82 lines
2.6 KiB
Go
82 lines
2.6 KiB
Go
package main
|
|
|
|
import (
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
|
|
"osdb/internal/handler"
|
|
"osdb/internal/repository"
|
|
)
|
|
|
|
// New creates and configures the Echo instance with all routes registered.
|
|
func New(pool *pgxpool.Pool) *echo.Echo {
|
|
e := echo.New()
|
|
e.HideBanner = true
|
|
|
|
e.Use(middleware.Logger())
|
|
e.Use(middleware.Recover())
|
|
|
|
health := handler.NewHealthHandler()
|
|
|
|
// Root health — used by docker healthcheck + ops tooling
|
|
e.GET("/health", health.Health)
|
|
|
|
// Versioned API group
|
|
api := e.Group("/api/v1")
|
|
api.GET("/health", health.Health)
|
|
|
|
// Fruits (story #02)
|
|
fruitRepo := repository.NewFruitRepo(pool)
|
|
fruits := handler.NewFruitHandler(fruitRepo)
|
|
|
|
g := api.Group("/fruits")
|
|
g.GET("", fruits.List)
|
|
g.POST("", fruits.Create)
|
|
g.GET("/:id", fruits.Get)
|
|
g.PUT("/:id", fruits.Update)
|
|
g.DELETE("/:id", fruits.Delete)
|
|
g.GET("/:id/images", fruits.ListImages)
|
|
g.POST("/:id/images", fruits.UploadImage, middleware.BodyLimit("5M"))
|
|
g.GET("/:id/images/:imageId", fruits.ServeImage)
|
|
g.GET("/:id/images/:imageId/thumbnail", fruits.ServeThumbnail)
|
|
g.DELETE("/:id/images/:imageId", fruits.DeleteImage)
|
|
|
|
// Publications (story #04)
|
|
pubRepo := repository.NewPublicationRepo(pool)
|
|
pubs := handler.NewPublicationHandler(pubRepo)
|
|
|
|
// Fruit-scoped publication reads
|
|
g.GET("/:id/descriptions", pubs.GetFruitDescriptions)
|
|
g.GET("/:id/publication-images", pubs.GetFruitPublicationImages)
|
|
|
|
p := api.Group("/publications")
|
|
p.GET("", pubs.List)
|
|
p.POST("", pubs.Create)
|
|
p.GET("/:id", pubs.Get)
|
|
p.PUT("/:id", pubs.Update)
|
|
p.DELETE("/:id", pubs.Delete)
|
|
p.POST("/:id/image", pubs.UploadCoverImage, middleware.BodyLimit("5M"))
|
|
p.GET("/:id/image", pubs.ServeCoverImage)
|
|
p.DELETE("/:id/image", pubs.DeleteCoverImage)
|
|
p.GET("/:id/fruits", pubs.ListFruits)
|
|
p.POST("/:id/fruits", pubs.LinkFruit)
|
|
p.DELETE("/:id/fruits/:fruitId", pubs.UnlinkFruit)
|
|
p.GET("/:id/descriptions", pubs.ListDescriptions)
|
|
p.POST("/:id/descriptions", pubs.UploadDescription, middleware.BodyLimit("5M"))
|
|
p.GET("/:id/descriptions/:descId", pubs.ServeDescription)
|
|
p.DELETE("/:id/descriptions/:descId", pubs.DeleteDescription)
|
|
p.GET("/:id/fruit-images", pubs.ListFruitImages)
|
|
p.POST("/:id/fruit-images", pubs.UploadFruitImage, middleware.BodyLimit("5M"))
|
|
p.GET("/:id/fruit-images/:imgId", pubs.ServeFruitImage)
|
|
p.GET("/:id/fruit-images/:imgId/thumbnail", pubs.ServeFruitImageThumbnail)
|
|
p.DELETE("/:id/fruit-images/:imgId", pubs.DeleteFruitImage)
|
|
|
|
// Admin (story #07)
|
|
admin := handler.NewAdminHandler(fruitRepo, pubRepo)
|
|
adminGroup := api.Group("/admin")
|
|
adminGroup.POST("/backfill-thumbnails", admin.BackfillThumbnails)
|
|
|
|
return e
|
|
}
|