package search import ( "database/sql" "log/slog" "net/http" "strconv" "github.com/mithral/vellum/internal/auth" "github.com/mithral/vellum/internal/httpx" ) // Handlers serves the search endpoint. type Handlers struct { db *sql.DB log *slog.Logger auth *auth.Authenticator } // NewHandlers builds the search HTTP layer. func NewHandlers(db *sql.DB, log *slog.Logger, a *auth.Authenticator) *Handlers { return &Handlers{db: db, log: log, auth: a} } // Register mounts the route. func (h *Handlers) Register(mux *http.ServeMux) { mux.Handle("GET /api/v1/search", h.auth.Require(h.handleSearch)) } func (h *Handlers) handleSearch(w http.ResponseWriter, r *http.Request, u auth.User) { q := r.URL.Query().Get("q") if q == "" { httpx.WriteJSON(w, http.StatusOK, map[string]any{"results": []Result{}, "query": ""}) return } limit := 50 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 200 { limit = n } } // Scope the search to libraries the caller may read. This is the same // chokepoint every other read path uses. libs, err := auth.VisibleLibraryIDs(r.Context(), h.db, u) if err != nil { h.log.Error("visible libraries", "err", err) httpx.WriteError(w, http.StatusInternalServerError, "internal server error") return } results, err := Query(r.Context(), h.db, q, libs, limit) if err != nil { h.log.Error("search", "err", err) httpx.WriteError(w, http.StatusInternalServerError, "internal server error") return } httpx.WriteJSON(w, http.StatusOK, map[string]any{"results": results, "query": q}) }