use axum::{
extract::Query,
Json,
};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::auth::AuthenticatedUser;
use crate::cedar::enforcement::TenantState;
use crate::error::Result;
use crate::models::{
generate_snippet, PaginatedResponse, SearchResult, TagSummary,
};
use crate::service::SearchService;
fn default_limit() -> i64 {
20
}
#[derive(Debug, Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
pub struct SearchParams {
#[serde(default = "default_limit")]
pub limit: i64,
#[serde(default)]
pub cursor: Option<String>,
pub q: Option<String>,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub full_content: bool,
}
fn parse_tag_filters(params: &SearchParams) -> Vec<(String, String)> {
let mut all_tags = params.tags.clone();
if let Some(tag) = ¶ms.tag {
all_tags.push(tag.clone());
}
all_tags
.iter()
.flat_map(|t| t.split(','))
.filter_map(|t| {
let parts: Vec<&str> = t.splitn(2, ':').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect()
}
#[utoipa::path(
get,
path = "/api/search",
params(SearchParams),
responses(
(status = 200, description = "Search results (compact SearchResult by default, full Asset when full_content=true)"),
),
tag = "search",
)]
pub async fn search(
tx: TenantState,
user: AuthenticatedUser,
Query(params): Query<SearchParams>,
) -> Result<Json<serde_json::Value>> {
let claims = crate::cedar::enforcement::extract_claims_for_cedar(&user);
let tag_filters = parse_tag_filters(¶ms);
let (assets, next_cursor) = SearchService::search(
tx.services(),
params.q.as_deref(),
tag_filters,
params.limit,
params.cursor.as_deref(),
)
.await?;
let visible_assets = if let Some(authorizer) = tx.authorizer() {
crate::cedar::enforcement::filter_by_permission(
authorizer,
&claims,
"View",
assets,
)
} else {
assets
};
if params.full_content {
Ok(Json(serde_json::to_value(PaginatedResponse {
data: visible_assets,
next_cursor,
total: None,
})?))
} else {
let results: Vec<SearchResult> = visible_assets
.into_iter()
.map(|a| SearchResult {
id: a.id,
title: a.title,
snippet: generate_snippet(a.content.as_deref().unwrap_or(""), 200),
tags: a
.tags
.into_iter()
.map(|t| TagSummary {
category: t.category,
value: t.value,
})
.collect(),
updated_at: a.updated_at,
})
.collect();
Ok(Json(serde_json::to_value(PaginatedResponse {
data: results,
next_cursor,
total: None,
})?))
}
}