//! Handlers for subject and predicate discovery endpoints. //! //! These endpoints scan existing Redb indexes to expose the subjects //! and predicates known to the system, enabling autocomplete/typeahead //! in the dashboard. use axum::{ extract::{Path, State}, Json, }; use tracing::instrument; use crate::{ dto::subjects::{ListPredicatesResponse, ListSubjectsParams, ListSubjectsResponse}, error::Result, extractors::QsQuery, state::AppState, }; use stemedb_storage::{key_codec, KVStore}; /// List all known subjects, with optional prefix filtering. /// /// Scans the `\x00SUBJECTS:` index in Redb. Supports prefix filtering /// via the `q` parameter for typeahead/autocomplete use cases. #[utoipa::path( get, path = "/v1/subjects", params( ("q" = Option, Query, description = "Prefix filter for subject names"), ("limit" = Option, Query, description = "Max results (default 100, max 1000)") ), responses( (status = 200, description = "List of subjects", body = ListSubjectsResponse), (status = 500, description = "Internal server error", body = crate::dto::ErrorResponse) ), tag = "discovery" )] #[instrument(skip(state), fields(q = ?params.q, limit = ?params.limit))] pub async fn list_subjects( State(state): State, QsQuery(params): QsQuery, ) -> Result> { metrics::counter!("stemedb_queries_total", "endpoint" => "list_subjects").increment(1); let prefix = if let Some(ref q) = params.q { key_codec::subjects_index_key(q) } else { key_codec::subjects_scan_prefix() }; let entries = state.store.scan_prefix(&prefix).await?; let total_count = entries.len(); let limit = params.limit.unwrap_or(100).min(1000); let subjects: Vec = entries .iter() .filter_map(|(k, _)| key_codec::extract_subject_from_subjects_key(k)) .take(limit) .collect(); Ok(Json(ListSubjectsResponse { subjects, total_count })) } /// List all predicates for a given subject. /// /// Scans the `{subject}\x00SP:` index in Redb to find all predicates /// that have been asserted for this subject. #[utoipa::path( get, path = "/v1/subjects/{subject}/predicates", params( ("subject" = String, Path, description = "The subject to list predicates for") ), responses( (status = 200, description = "List of predicates for the subject", body = ListPredicatesResponse), (status = 500, description = "Internal server error", body = crate::dto::ErrorResponse) ), tag = "discovery" )] #[instrument(skip(state), fields(%subject))] pub async fn list_predicates( State(state): State, Path(subject): Path, ) -> Result> { metrics::counter!("stemedb_queries_total", "endpoint" => "list_predicates").increment(1); let prefix = key_codec::subject_predicate_scan_prefix(&subject); let entries = state.store.scan_prefix(&prefix).await?; let predicates: Vec = entries.iter().filter_map(|(k, _)| key_codec::extract_sp_key(k).map(|(_, p)| p)).collect(); Ok(Json(ListPredicatesResponse { subject, predicates })) }