use crate::auth_catalog::build_auth_catalog;
use crate::commands::plan::{PlanOptions, plan_node, select_root};
use crate::error::CliError;
use crate::serve::error::ServeError;
use crate::serve::load::load_submission;
use crate::serve::rbac::AuthContext;
use crate::serve::runner::ConfigFormatWire;
use crate::serve::state::ServerState;
use axum::Json;
use axum::extract::{Extension, State};
use serde::Deserialize;
use serde_json::Value;
fn cli_to_serve(e: CliError) -> ServeError {
match e {
CliError::Config(m) => ServeError::BadConfig(m),
other => ServeError::Internal(other.to_string()),
}
}
#[derive(Debug, Deserialize)]
pub struct PlanRequest {
pub config: String,
#[serde(default)]
pub config_format: ConfigFormatWire,
#[serde(default)]
pub row: Option<String>,
#[serde(default)]
pub sample: Option<Vec<Value>>,
#[serde(default)]
pub impact: bool,
#[serde(default)]
pub depth: Option<u32>,
}
pub async fn plan(
State(state): State<ServerState>,
Extension(actor): Extension<AuthContext>,
Json(req): Json<PlanRequest>,
) -> Result<Json<Value>, ServeError> {
let loaded = load_submission(
&req.config,
req.config_format.into(),
state.default_base().as_ref(),
crate::serve::runner::server_policy(&state).as_deref(),
)
.await?;
let auth = build_auth_catalog(loaded.cfg.auth.as_ref()).map_err(cli_to_serve)?;
let node = select_root(&loaded.nodes, req.row.as_deref()).map_err(cli_to_serve)?;
let sample = req.sample.map(|records| {
let label = format!("request ({} record(s))", records.len());
(records, label)
});
#[cfg(feature = "catalog")]
let history = state.history();
#[cfg(feature = "catalog")]
let impact = req.impact.then(|| crate::commands::plan::ImpactOptions {
store: history.as_ref(),
pipeline: loaded
.cfg
.name
.clone()
.unwrap_or_else(|| "serve".to_string()),
depth: req.depth.unwrap_or(crate::impact::DEFAULT_DEPTH),
});
#[cfg(not(feature = "catalog"))]
if req.impact {
return Err(ServeError::Unprocessable {
message: "impact analysis requires a server built with the `catalog` feature".into(),
details: None,
});
}
let report = plan_node(
&loaded.cfg,
node,
&auth,
PlanOptions {
sample,
#[cfg(feature = "catalog")]
impact,
#[cfg(not(feature = "catalog"))]
_marker: std::marker::PhantomData,
},
)
.await
.map_err(cli_to_serve)?;
let value = serde_json::to_value(&report).map_err(|e| ServeError::Internal(e.to_string()))?;
crate::serve::audit::write(&state, &actor, "plan", None, None, "ok").await;
Ok(Json(value))
}