use std::str::FromStr;
use std::sync::Arc;
use jammi_ai::session::InferenceSession;
use jammi_ai::Session;
use jammi_db::catalog::channel_repo::ChannelSpec;
use jammi_db::source::SourceConnection;
use jammi_db::trigger::ids::TopicId;
use jammi_db::trigger::{TopicDefinition, TriggerError};
use jammi_db::TenantId;
use jammi_wire::{
channel_to_proto, columns_from_proto, definition_from_proto, definition_to_proto,
model_to_proto, parse_channel_id, parse_table_id, source_type_from_proto, topic_to_proto,
};
use tonic::{Request, Response, Status};
use crate::grpc::proto::catalog as pb;
use crate::grpc::proto::catalog::catalog_service_server::CatalogService;
use crate::grpc::session::{SessionId, SessionStore, SESSION_HEADER};
use crate::grpc::wire::{
map_engine_error, map_trigger_error, require_nonempty, scoped, session_tenant,
};
use crate::tiers::TierSet;
pub struct CatalogServer {
store: SessionStore,
tiers: TierSet,
session: Option<Arc<InferenceSession>>,
}
impl CatalogServer {
pub fn new(
store: SessionStore,
tiers: TierSet,
session: Option<Arc<InferenceSession>>,
) -> Self {
Self {
store,
tiers,
session,
}
}
fn engine(&self) -> Result<&Arc<InferenceSession>, Status> {
self.session.as_ref().ok_or_else(|| {
Status::unavailable("catalog engine verbs are not enabled on this deployment")
})
}
fn local(&self) -> Result<Session, Status> {
Ok(Session::new(Arc::clone(self.engine()?)))
}
}
#[tonic::async_trait]
impl CatalogService for CatalogServer {
async fn set_tenant(
&self,
request: Request<pb::SetTenantRequest>,
) -> Result<Response<()>, Status> {
let session = session_id_from_request(&request)?;
let body = request.into_inner();
let tenant = body
.tenant
.ok_or_else(|| Status::invalid_argument("tenant field is required"))?;
let parsed = parse_tenant(&tenant)?;
self.store.set(session, parsed);
Ok(Response::new(()))
}
async fn get_tenant(
&self,
request: Request<()>,
) -> Result<Response<pb::GetTenantResponse>, Status> {
let session = session_id_from_request(&request)?;
let tenant = self.store.get(&session);
Ok(Response::new(pb::GetTenantResponse {
tenant: Some(pb::Tenant {
id: tenant.map(|t| t.to_string()).unwrap_or_default(),
}),
}))
}
async fn clear_tenant(&self, request: Request<()>) -> Result<Response<()>, Status> {
let session = session_id_from_request(&request)?;
self.store.clear(&session);
Ok(Response::new(()))
}
async fn get_server_info(
&self,
_request: Request<()>,
) -> Result<Response<pb::ServerInfo>, Status> {
let info = jammi_db::ServerInfo::current();
Ok(Response::new(pb::ServerInfo {
version: info.version,
features: info.features,
storage_backends: info.storage_backends,
services: self.tiers.as_wire(),
}))
}
async fn add_source(
&self,
request: Request<pb::AddSourceRequest>,
) -> Result<Response<()>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
require_nonempty(&req.source_id, "source_id")?;
let source_type = source_type_from_proto(req.source_kind)?;
let connection: SourceConnection = req
.connection
.ok_or_else(|| Status::invalid_argument("connection is required"))?
.try_into()?;
let session = self.local()?;
scoped(self.engine()?, tenant, || {
session.add_source(&req.source_id, source_type, connection)
})
.await
.map_err(map_engine_error)?;
Ok(Response::new(()))
}
async fn remove_source(
&self,
request: Request<pb::RemoveSourceRequest>,
) -> Result<Response<()>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
require_nonempty(&req.source_id, "source_id")?;
let session = self.local()?;
scoped(self.engine()?, tenant, || {
session.remove_source(&req.source_id)
})
.await
.map_err(map_engine_error)?;
Ok(Response::new(()))
}
async fn list_sources(
&self,
request: Request<pb::ListSourcesRequest>,
) -> Result<Response<pb::ListSourcesResponse>, Status> {
let tenant = session_tenant(&request);
let session = self.local()?;
let descriptors = scoped(self.engine()?, tenant, || session.list_sources())
.await
.map_err(map_engine_error)?;
let sources = descriptors
.into_iter()
.map(pb::SourceDescriptor::from)
.collect();
Ok(Response::new(pb::ListSourcesResponse { sources }))
}
async fn describe_source(
&self,
request: Request<pb::DescribeSourceRequest>,
) -> Result<Response<pb::SourceDescriptor>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
require_nonempty(&req.source_id, "source_id")?;
let session = self.local()?;
let descriptor = scoped(self.engine()?, tenant, || {
session.describe_source(&req.source_id)
})
.await
.map_err(map_engine_error)?
.ok_or_else(|| Status::not_found(format!("source '{}' not found", req.source_id)))?;
Ok(Response::new(pb::SourceDescriptor::from(descriptor)))
}
async fn list_models(
&self,
request: Request<pb::ListModelsRequest>,
) -> Result<Response<pb::ListModelsResponse>, Status> {
let tenant = session_tenant(&request);
let session = self.local()?;
let records = scoped(self.engine()?, tenant, || session.list_models())
.await
.map_err(map_engine_error)?;
let models = records.iter().map(model_to_proto).collect();
Ok(Response::new(pb::ListModelsResponse { models }))
}
async fn describe_model(
&self,
request: Request<pb::DescribeModelRequest>,
) -> Result<Response<pb::Model>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
require_nonempty(&req.model_id, "model_id")?;
let session = self.local()?;
let record = scoped(self.engine()?, tenant, || {
session.describe_model(&req.model_id)
})
.await
.map_err(map_engine_error)?
.ok_or_else(|| Status::not_found(format!("model '{}' not found", req.model_id)))?;
Ok(Response::new(model_to_proto(&record)))
}
async fn register_channel(
&self,
request: Request<pb::RegisterChannelRequest>,
) -> Result<Response<()>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
let id = parse_channel_id(&req.channel_id)?;
let columns = columns_from_proto(req.columns)?;
let spec = ChannelSpec {
id,
priority: req.priority,
columns,
};
let session = self.local()?;
scoped(self.engine()?, tenant, || session.register_channel(&spec))
.await
.map_err(map_engine_error)?;
Ok(Response::new(()))
}
async fn add_channel_columns(
&self,
request: Request<pb::AddChannelColumnsRequest>,
) -> Result<Response<()>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
let id = parse_channel_id(&req.channel_id)?;
let columns = columns_from_proto(req.columns)?;
let session = self.local()?;
scoped(self.engine()?, tenant, || {
session.add_channel_columns(&id, &columns)
})
.await
.map_err(map_engine_error)?;
Ok(Response::new(()))
}
async fn list_channels(
&self,
request: Request<pb::ListChannelsRequest>,
) -> Result<Response<pb::ListChannelsResponse>, Status> {
let tenant = session_tenant(&request);
let session = self.local()?;
let specs = scoped(self.engine()?, tenant, || session.list_channels())
.await
.map_err(map_engine_error)?;
let channels = specs.iter().map(channel_to_proto).collect();
Ok(Response::new(pb::ListChannelsResponse { channels }))
}
async fn create_mutable_table(
&self,
request: Request<pb::CreateMutableTableRequest>,
) -> Result<Response<pb::CreateMutableTableResponse>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
let def_proto = req
.definition
.ok_or_else(|| Status::invalid_argument("definition is required"))?;
let def = definition_from_proto(def_proto, tenant)?;
let session = self.local()?;
let id = scoped(self.engine()?, tenant, || session.create_mutable_table(def))
.await
.map_err(map_engine_error)?;
Ok(Response::new(pb::CreateMutableTableResponse {
mutable_table_id: id.to_string(),
}))
}
async fn drop_mutable_table(
&self,
request: Request<pb::DropMutableTableRequest>,
) -> Result<Response<()>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
let id = parse_table_id(&req.mutable_table_id)?;
let session = self.local()?;
scoped(self.engine()?, tenant, || session.drop_mutable_table(&id))
.await
.map_err(map_engine_error)?;
Ok(Response::new(()))
}
async fn list_mutable_tables(
&self,
request: Request<pb::ListMutableTablesRequest>,
) -> Result<Response<pb::ListMutableTablesResponse>, Status> {
let tenant = session_tenant(&request);
let session = self.local()?;
let defs = scoped(self.engine()?, tenant, || session.list_mutable_tables())
.await
.map_err(map_engine_error)?;
let definitions = defs
.iter()
.map(definition_to_proto)
.collect::<Result<Vec<_>, Status>>()?;
Ok(Response::new(pb::ListMutableTablesResponse { definitions }))
}
async fn register_topic(
&self,
request: Request<pb::RegisterTopicRequest>,
) -> Result<Response<pb::RegisterTopicResponse>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
if req.name.is_empty() {
return Err(Status::invalid_argument("name is required"));
}
let schema = jammi_wire::decode_ipc_schema(&req.schema)?;
let broker_metadata = req.broker_metadata.into_iter().collect();
let id = if req.topic_id.is_empty() {
TopicId::new()
} else {
TopicId::from_str(&req.topic_id)
.map_err(|e| Status::invalid_argument(format!("invalid topic_id: {e}")))?
};
let topic = TopicDefinition {
id,
name: req.name,
schema,
tenant,
broker_metadata,
};
let session = self.local()?;
scoped(self.engine()?, tenant, || session.register_topic(&topic))
.await
.map_err(map_trigger_error)?;
Ok(Response::new(pb::RegisterTopicResponse {
topic_id: topic.id.to_string(),
}))
}
async fn drop_topic(
&self,
request: Request<pb::DropTopicRequest>,
) -> Result<Response<()>, Status> {
let tenant = session_tenant(&request);
let req = request.into_inner();
if req.topic_id.is_empty() {
return Err(Status::invalid_argument("topic_id is required"));
}
let topic_id = TopicId::from_str(&req.topic_id)
.map_err(|e| Status::invalid_argument(format!("invalid topic_id: {e}")))?;
let session = self.local()?;
match scoped(self.engine()?, tenant, || session.drop_topic(topic_id)).await {
Ok(()) => Ok(Response::new(())),
Err(TriggerError::TopicNotFound(_)) if req.if_exists => Ok(Response::new(())),
Err(e) => Err(map_trigger_error(e)),
}
}
async fn list_topics(
&self,
request: Request<pb::ListTopicsRequest>,
) -> Result<Response<pb::ListTopicsResponse>, Status> {
let tenant = session_tenant(&request);
let session = self.local()?;
let topics = scoped(self.engine()?, tenant, || session.list_topics())
.await
.map_err(map_trigger_error)?;
let topics = topics
.iter()
.map(topic_to_proto)
.collect::<Result<Vec<_>, Status>>()?;
Ok(Response::new(pb::ListTopicsResponse {
topics,
next_page_token: String::new(),
}))
}
}
fn session_id_from_request<T>(request: &Request<T>) -> Result<SessionId, Status> {
request
.metadata()
.get(SESSION_HEADER)
.and_then(|v| v.to_str().ok())
.map(SessionId::new)
.ok_or_else(|| {
Status::invalid_argument(format!(
"request missing required `{SESSION_HEADER}` metadata header"
))
})
}
fn parse_tenant(t: &pb::Tenant) -> Result<Option<TenantId>, Status> {
if t.id.is_empty() {
return Ok(None);
}
TenantId::from_str(&t.id)
.map(Some)
.map_err(|e| Status::invalid_argument(format!("invalid tenant id: {e}")))
}