Files
food-ai/backend/cmd/server/providers.go
dbastrikin 205edbdade feat: rename ingredients→products, products→user_products; add barcode/OFF import
- Rename catalog: ingredient/* → product/* (canonical_name, barcode, nutrition per 100g)
- Rename pantry: product/* → userproduct/* (user-owned items with expiry)
- Squash migrations into single 001_initial_schema.sql (clean-db baseline)
- product_categories: add English canonical name column; fix COALESCE in queries
- Remove product_translations: product names are stored in their original language
- Add default_unit_name to product API responses via unit_translations JOIN
- Add cmd/importoff: bulk import from OpenFoodFacts JSONL dump (COPY + ON CONFLICT)
- Diary: support product_id entries alongside dish_id (CHECK num_nonnulls = 1)
- Home: getLoggedCalories joins both recipes and catalog products
- Flutter: rename models/providers/services to match backend rename
- Flutter: add barcode scan flow for diary (mobile_scanner, product_portion_sheet)
- Flutter: localise 6 new keys across 12 languages (barcode scan, portion weight)
- Routes: GET /products/search, GET /products/barcode/{barcode}, /user-products

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 12:45:48 +02:00

216 lines
7.5 KiB
Go

package main
import (
"net/http"
"time"
"github.com/food-ai/backend/internal/domain/auth"
"github.com/food-ai/backend/internal/infra/config"
"github.com/food-ai/backend/internal/domain/diary"
"github.com/food-ai/backend/internal/domain/dish"
"github.com/food-ai/backend/internal/adapters/kafka"
"github.com/food-ai/backend/internal/adapters/openai"
"github.com/food-ai/backend/internal/domain/home"
"github.com/food-ai/backend/internal/domain/menu"
"github.com/food-ai/backend/internal/infra/middleware"
"github.com/food-ai/backend/internal/adapters/pexels"
"github.com/food-ai/backend/internal/domain/product"
"github.com/food-ai/backend/internal/domain/recipe"
"github.com/food-ai/backend/internal/domain/recognition"
"github.com/food-ai/backend/internal/domain/recommendation"
"github.com/food-ai/backend/internal/domain/savedrecipe"
"github.com/food-ai/backend/internal/domain/cuisine"
"github.com/food-ai/backend/internal/infra/server"
"github.com/food-ai/backend/internal/domain/tag"
"github.com/food-ai/backend/internal/domain/units"
"github.com/food-ai/backend/internal/domain/user"
"github.com/food-ai/backend/internal/domain/userproduct"
"github.com/jackc/pgx/v5/pgxpool"
)
// ---------------------------------------------------------------------------
// Newtypes for config primitives — prevents Wire type collisions.
// ---------------------------------------------------------------------------
// Newtypes for list handlers — prevents Wire type collisions on http.HandlerFunc.
type unitsListHandler http.HandlerFunc
type cuisineListHandler http.HandlerFunc
type tagListHandler http.HandlerFunc
// ---------------------------------------------------------------------------
type openaiAPIKey string
type pexelsAPIKey string
type jwtSecret string
type jwtAccessDuration time.Duration
type jwtRefreshDuration time.Duration
type allowedOrigins []string
// ---------------------------------------------------------------------------
// Config extractors
// ---------------------------------------------------------------------------
func newOpenAIAPIKey(appConfig *config.Config) openaiAPIKey {
return openaiAPIKey(appConfig.OpenAIAPIKey)
}
func newPexelsAPIKey(appConfig *config.Config) pexelsAPIKey {
return pexelsAPIKey(appConfig.PexelsAPIKey)
}
func newJWTSecret(appConfig *config.Config) jwtSecret {
return jwtSecret(appConfig.JWTSecret)
}
func newJWTAccessDuration(appConfig *config.Config) jwtAccessDuration {
return jwtAccessDuration(appConfig.JWTAccessDuration)
}
func newJWTRefreshDuration(appConfig *config.Config) jwtRefreshDuration {
return jwtRefreshDuration(appConfig.JWTRefreshDuration)
}
func newAllowedOrigins(appConfig *config.Config) allowedOrigins {
return allowedOrigins(appConfig.AllowedOrigins)
}
// newFirebaseCredentialsFile is the only string that reaches NewFirebaseAuthOrNoop,
// so no newtype is needed here.
func newFirebaseCredentialsFile(appConfig *config.Config) string {
return appConfig.FirebaseCredentialsFile
}
// ---------------------------------------------------------------------------
// Constructor wrappers for functions that accept primitive newtypes
// ---------------------------------------------------------------------------
func newOpenAIClient(key openaiAPIKey) *openai.Client {
return openai.NewClient(string(key))
}
func newPexelsClient(key pexelsAPIKey) *pexels.Client {
return pexels.NewClient(string(key))
}
func newJWTManager(
secret jwtSecret,
accessDuration jwtAccessDuration,
refreshDuration jwtRefreshDuration,
) *auth.JWTManager {
return auth.NewJWTManager(string(secret), time.Duration(accessDuration), time.Duration(refreshDuration))
}
// ---------------------------------------------------------------------------
// List handler providers
// ---------------------------------------------------------------------------
func newUnitsListHandler(pool *pgxpool.Pool) unitsListHandler {
return unitsListHandler(units.NewListHandler(pool))
}
func newCuisineListHandler(pool *pgxpool.Pool) cuisineListHandler {
return cuisineListHandler(cuisine.NewListHandler(pool))
}
func newTagListHandler(pool *pgxpool.Pool) tagListHandler {
return tagListHandler(tag.NewListHandler(pool))
}
// ---------------------------------------------------------------------------
// newRouter wraps server.NewRouter to accept newtypes.
func newRouter(
pool *pgxpool.Pool,
authHandler *auth.Handler,
userHandler *user.Handler,
recommendationHandler *recommendation.Handler,
savedRecipeHandler *savedrecipe.Handler,
productHandler *product.Handler,
userProductHandler *userproduct.Handler,
recognitionHandler *recognition.Handler,
menuHandler *menu.Handler,
diaryHandler *diary.Handler,
homeHandler *home.Handler,
dishHandler *dish.Handler,
recipeHandler *recipe.Handler,
authMiddleware func(http.Handler) http.Handler,
origins allowedOrigins,
unitsHandler unitsListHandler,
cuisineHandler cuisineListHandler,
tagHandler tagListHandler,
) http.Handler {
return server.NewRouter(
pool,
authHandler,
userHandler,
recommendationHandler,
savedRecipeHandler,
productHandler,
userProductHandler,
recognitionHandler,
menuHandler,
diaryHandler,
homeHandler,
dishHandler,
recipeHandler,
authMiddleware,
[]string(origins),
http.HandlerFunc(unitsHandler),
http.HandlerFunc(cuisineHandler),
http.HandlerFunc(tagHandler),
)
}
// ---------------------------------------------------------------------------
// jwtAdapter — adapts *auth.JWTManager to middleware.AccessTokenValidator.
// ---------------------------------------------------------------------------
type jwtAdapter struct {
jwtManager *auth.JWTManager
}
func newJWTAdapter(jm *auth.JWTManager) *jwtAdapter {
return &jwtAdapter{jwtManager: jm}
}
func (adapter *jwtAdapter) ValidateAccessToken(tokenStr string) (*middleware.TokenClaims, error) {
claims, validationError := adapter.jwtManager.ValidateAccessToken(tokenStr)
if validationError != nil {
return nil, validationError
}
return &middleware.TokenClaims{
UserID: claims.UserID,
Plan: claims.Plan,
}, nil
}
// newAuthMiddleware wraps middleware.Auth for Wire injection.
func newAuthMiddleware(validator middleware.AccessTokenValidator) func(http.Handler) http.Handler {
return middleware.Auth(validator)
}
// ---------------------------------------------------------------------------
// Kafka providers
// ---------------------------------------------------------------------------
func newKafkaProducer(appConfig *config.Config) (*kafka.Producer, error) {
return kafka.NewProducer(appConfig.KafkaBrokers)
}
// ---------------------------------------------------------------------------
// Interface assertions (compile-time checks)
// ---------------------------------------------------------------------------
var _ middleware.AccessTokenValidator = (*jwtAdapter)(nil)
var _ menu.PhotoSearcher = (*pexels.Client)(nil)
var _ menu.UserLoader = (*user.Repository)(nil)
var _ menu.ProductLister = (*userproduct.Repository)(nil)
var _ menu.RecipeSaver = (*dish.Repository)(nil)
var _ recommendation.PhotoSearcher = (*pexels.Client)(nil)
var _ recommendation.UserLoader = (*user.Repository)(nil)
var _ recommendation.ProductLister = (*userproduct.Repository)(nil)
var _ recognition.ProductRepository = (*product.Repository)(nil)
var _ recognition.KafkaPublisher = (*kafka.Producer)(nil)
var _ recognition.JobRepository = (*recognition.PostgresJobRepository)(nil)
var _ user.UserRepository = (*user.Repository)(nil)