use crate::api::auth::{AuthManager, Credentials};
use crate::api::CardListParams;
use crate::core::error::{Error, Result};
use crate::core::models::common::{CardId, ExportFormat};
#[cfg(feature = "query-builder")]
use crate::core::models::mbql::MbqlQuery;
use crate::core::models::{
Card, Collection, Dashboard, DatabaseMetadata, DatasetQuery, Field, HealthStatus, MetabaseId,
NativeQuery, Pagination, QueryResult, SyncResult, User,
};
use crate::service::ServiceManager;
use crate::transport::http_provider_safe::{HttpClientAdapter, HttpProviderSafe};
use crate::transport::HttpClient;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(feature = "cache")]
use crate::cache::{cache_key, CacheConfig, CacheLayer};
#[derive(Clone)]
pub struct MetabaseClient {
pub(super) auth_manager: AuthManager,
pub(super) base_url: String,
pub(super) service_manager: ServiceManager,
#[cfg(feature = "cache")]
pub(super) cache: CacheLayer,
}
impl MetabaseClient {
pub fn new(base_url: impl Into<String>) -> Result<Self> {
let base_url = base_url.into();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
return Err(Error::Config(
"Invalid URL: must start with http:// or https://".to_string(),
));
}
let http_client = HttpClient::new(&base_url)?;
let auth_manager = AuthManager::new();
let http_provider: Arc<dyn HttpProviderSafe> =
Arc::new(HttpClientAdapter::new(http_client));
let service_manager = ServiceManager::new(http_provider);
Ok(Self {
auth_manager,
base_url,
service_manager,
#[cfg(feature = "cache")]
cache: CacheLayer::new(CacheConfig::default()),
})
}
#[cfg(feature = "cache")]
pub fn with_cache(base_url: impl Into<String>, cache_config: CacheConfig) -> Result<Self> {
let base_url = base_url.into();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
return Err(Error::Config(
"Invalid URL: must start with http:// or https://".to_string(),
));
}
let http_client = HttpClient::new(&base_url)?;
let auth_manager = AuthManager::new();
let http_provider: Arc<dyn HttpProviderSafe> =
Arc::new(HttpClientAdapter::new(http_client));
let service_manager = ServiceManager::new(http_provider);
Ok(Self {
auth_manager,
base_url,
service_manager,
cache: CacheLayer::new(cache_config),
})
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn is_authenticated(&self) -> bool {
self.auth_manager.is_authenticated()
}
#[cfg(feature = "cache")]
pub fn is_cache_enabled(&self) -> bool {
self.cache.is_enabled()
}
#[cfg(feature = "cache")]
pub fn set_cache_enabled(&mut self, enabled: bool) {
self.cache.set_enabled(enabled);
}
#[cfg(not(feature = "cache"))]
pub fn is_cache_enabled(&self) -> bool {
false
}
#[cfg(not(feature = "cache"))]
pub fn set_cache_enabled(&mut self, _enabled: bool) {
}
pub async fn authenticate(&mut self, credentials: Credentials) -> Result<()> {
let (session_id, user) = self
.service_manager
.auth_service()
.ok_or_else(|| Error::Config("Auth service not available".to_string()))?
.authenticate(credentials)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
self.auth_manager.set_session(session_id, user);
Ok(())
}
pub async fn logout(&mut self) -> Result<()> {
if !self.is_authenticated() {
return Ok(());
}
let session_id = self.auth_manager.get_session_id();
if let Some(id) = session_id {
self.service_manager
.auth_service()
.ok_or_else(|| Error::Config("Auth service not available".to_string()))?
.logout(&id)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
}
self.auth_manager.clear_session();
Ok(())
}
pub async fn health_check(&self) -> Result<HealthStatus> {
self.service_manager
.auth_service()
.ok_or_else(|| Error::Config("Auth service not available".to_string()))?
.health_check()
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn get_current_user(&self) -> Result<User> {
if !self.is_authenticated() {
return Err(Error::Authentication("Not authenticated".to_string()));
}
let session_id = self
.auth_manager
.get_session_id()
.ok_or_else(|| Error::Authentication("No session available".to_string()))?;
self.service_manager
.auth_service()
.ok_or_else(|| Error::Config("Auth service not available".to_string()))?
.get_current_user(&session_id)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn get_card(&self, id: i64) -> Result<Card> {
#[cfg(feature = "cache")]
{
let cache_key = cache_key("card", id);
if let Some(card) = self.cache.get_metadata::<Card>(&cache_key) {
return Ok(card);
}
}
let card = self
.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?
.get_card(CardId(id as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
#[cfg(feature = "cache")]
{
let cache_key = cache_key("card", id);
let _ = self.cache.set_metadata(cache_key, &card);
}
Ok(card)
}
pub async fn list_cards(&self, params: Option<CardListParams>) -> Result<Vec<Card>> {
use crate::repository::card::CardFilterParams;
let filters = params.map(|p| CardFilterParams {
f: p.f,
model_type: p.model_type,
archived: None,
collection_id: None,
});
self.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?
.list_cards(None, filters)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn create_card(&self, card: Card) -> Result<Card> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to create card".to_string(),
));
}
self.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?
.create_card(card)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn update_card(&self, id: i64, updates: serde_json::Value) -> Result<Card> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to update card".to_string(),
));
}
#[cfg(feature = "cache")]
{
let cache_key = cache_key("card", id);
self.cache.invalidate(&cache_key);
}
let service = self
.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?;
let mut existing_card = service
.get_card(CardId(id as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
if let Some(name) = updates.get("name").and_then(|v| v.as_str()) {
existing_card.name = name.to_string();
}
if let Some(description) = updates.get("description") {
existing_card.description = if description.is_null() {
None
} else {
description.as_str().map(|s| s.to_string())
};
}
if let Some(display) = updates.get("display").and_then(|v| v.as_str()) {
existing_card.display = display.to_string();
}
if let Some(dataset_query) = updates.get("dataset_query") {
existing_card.dataset_query = Some(dataset_query.clone());
}
if let Some(visualization_settings) = updates.get("visualization_settings") {
existing_card.visualization_settings = visualization_settings.clone();
}
service
.update_card(CardId(id as i32), existing_card)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn delete_card(&self, id: i64) -> Result<()> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to delete card".to_string(),
));
}
#[cfg(feature = "cache")]
{
let cache_key = cache_key("card", id);
self.cache.invalidate(&cache_key);
}
self.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?
.delete_card(CardId(id as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn get_collection(&self, id: MetabaseId) -> Result<Collection> {
use crate::core::models::common::CollectionId;
#[cfg(feature = "cache")]
{
let cache_key = cache_key("collection", id.0);
if let Some(collection) = self.cache.get_metadata::<Collection>(&cache_key) {
return Ok(collection);
}
}
let collection = self
.service_manager
.collection_service()
.ok_or_else(|| Error::Config("Collection service not available".to_string()))?
.get_collection(CollectionId(id.0 as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
#[cfg(feature = "cache")]
{
let cache_key = cache_key("collection", id.0);
let _ = self.cache.set_metadata(cache_key, &collection);
}
Ok(collection)
}
pub async fn list_collections(&self) -> Result<Vec<Collection>> {
self.service_manager
.collection_service()
.ok_or_else(|| Error::Config("Collection service not available".to_string()))?
.list_collections(None, None)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn create_collection(&self, collection: Collection) -> Result<Collection> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to create collection".to_string(),
));
}
self.service_manager
.collection_service()
.ok_or_else(|| Error::Config("Collection service not available".to_string()))?
.create_collection(collection)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn update_collection(
&self,
id: MetabaseId,
updates: serde_json::Value,
) -> Result<Collection> {
use crate::core::models::common::CollectionId;
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to update collection".to_string(),
));
}
#[cfg(feature = "cache")]
{
let cache_key = cache_key("collection", id.0);
self.cache.invalidate(&cache_key);
}
let service = self
.service_manager
.collection_service()
.ok_or_else(|| Error::Config("Collection service not available".to_string()))?;
let mut existing_collection = service
.get_collection(CollectionId(id.0 as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
if let Some(name) = updates.get("name").and_then(|v| v.as_str()) {
existing_collection.name = name.to_string();
}
if let Some(description) = updates.get("description") {
existing_collection.description = if description.is_null() {
None
} else {
description.as_str().map(|s| s.to_string())
};
}
if let Some(parent_id) = updates.get("parent_id") {
existing_collection.parent_id = if parent_id.is_null() {
None
} else {
parent_id.as_i64().map(|id| id as i32)
};
}
if let Some(color) = updates.get("color") {
existing_collection.color = if color.is_null() {
None
} else {
color.as_str().map(|s| s.to_string())
};
}
if let Some(archived) = updates.get("archived").and_then(|v| v.as_bool()) {
existing_collection.archived = Some(archived);
}
service
.update_collection(CollectionId(id.0 as i32), existing_collection)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn archive_collection(&self, id: MetabaseId) -> Result<Collection> {
use crate::core::models::common::CollectionId;
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to archive collection".to_string(),
));
}
#[cfg(feature = "cache")]
{
let cache_key = cache_key("collection", id.0);
self.cache.invalidate(&cache_key);
}
self.service_manager
.collection_service()
.ok_or_else(|| Error::Config("Collection service not available".to_string()))?
.archive_collection(CollectionId(id.0 as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
self.get_collection(id).await
}
pub async fn get_dashboard(&self, id: MetabaseId) -> Result<Dashboard> {
use crate::core::models::common::DashboardId;
#[cfg(feature = "cache")]
{
let cache_key = cache_key("dashboard", id.0);
if let Some(dashboard) = self.cache.get_metadata::<Dashboard>(&cache_key) {
return Ok(dashboard);
}
}
let dashboard = self
.service_manager
.dashboard_service()
.ok_or_else(|| Error::Config("Dashboard service not available".to_string()))?
.get_dashboard(DashboardId(id.0 as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
#[cfg(feature = "cache")]
{
let cache_key = cache_key("dashboard", id.0);
let _ = self.cache.set_metadata(cache_key, &dashboard);
}
Ok(dashboard)
}
pub async fn list_dashboards(&self, pagination: Option<Pagination>) -> Result<Vec<Dashboard>> {
use crate::repository::traits::PaginationParams;
let pagination_params = pagination.map(|p| PaginationParams {
page: None, limit: Some(p.limit() as u32),
offset: Some(p.offset() as u32),
});
self.service_manager
.dashboard_service()
.ok_or_else(|| Error::Config("Dashboard service not available".to_string()))?
.list_dashboards(pagination_params, None)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn create_dashboard(&self, dashboard: Dashboard) -> Result<Dashboard> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to create dashboard".to_string(),
));
}
self.service_manager
.dashboard_service()
.ok_or_else(|| Error::Config("Dashboard service not available".to_string()))?
.create_dashboard(dashboard)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn update_dashboard(
&self,
id: MetabaseId,
updates: serde_json::Value,
) -> Result<Dashboard> {
use crate::core::models::common::DashboardId;
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to update dashboard".to_string(),
));
}
#[cfg(feature = "cache")]
{
let cache_key = cache_key("dashboard", id.0);
self.cache.invalidate(&cache_key);
}
let dashboard: Dashboard = serde_json::from_value(updates)
.map_err(|e| Error::Validation(format!("Invalid dashboard update: {}", e)))?;
self.service_manager
.dashboard_service()
.ok_or_else(|| Error::Config("Dashboard service not available".to_string()))?
.update_dashboard(DashboardId(id.0 as i32), dashboard)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn delete_dashboard(&self, id: MetabaseId) -> Result<()> {
use crate::core::models::common::DashboardId;
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to delete dashboard".to_string(),
));
}
#[cfg(feature = "cache")]
{
let cache_key = cache_key("dashboard", id.0);
self.cache.invalidate(&cache_key);
}
self.service_manager
.dashboard_service()
.ok_or_else(|| Error::Config("Dashboard service not available".to_string()))?
.delete_dashboard(DashboardId(id.0 as i32))
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_query(&self, query: DatasetQuery) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute query".to_string(),
));
}
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.execute_dataset_query(query)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_native_query(
&self,
database: MetabaseId,
native_query: NativeQuery,
) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute native query".to_string(),
));
}
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.execute_native_query(database.0 as i32, native_query)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_card_query(
&self,
card_id: i64,
parameters: Option<Value>,
) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute card query".to_string(),
));
}
self.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?
.execute_card_query(CardId(card_id as i32), parameters)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn export_card_query(
&self,
card_id: i64,
format: ExportFormat,
parameters: Option<Value>,
) -> Result<Vec<u8>> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to export card query".to_string(),
));
}
self.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?
.export_card_query(CardId(card_id as i32), format, parameters)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_card_pivot_query(
&self,
card_id: i64,
parameters: Option<Value>,
) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute pivot query".to_string(),
));
}
self.service_manager
.card_service()
.ok_or_else(|| Error::Config("Card service not available".to_string()))?
.execute_card_pivot_query(CardId(card_id as i32), parameters)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn get_database_metadata(&self, database_id: MetabaseId) -> Result<DatabaseMetadata> {
#[cfg(feature = "cache")]
{
let cache_key = cache_key("database_metadata", database_id.0);
if let Some(metadata) = self.cache.get_metadata::<DatabaseMetadata>(&cache_key) {
return Ok(metadata);
}
}
let metadata = self
.service_manager
.database_service()
.ok_or_else(|| Error::Config("Database service not available".to_string()))?
.get_database_metadata(database_id)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))?;
#[cfg(feature = "cache")]
{
let cache_key = cache_key("database_metadata", database_id.0);
let _ = self.cache.set_metadata(cache_key, &metadata);
}
Ok(metadata)
}
pub async fn sync_database_schema(&self, database_id: MetabaseId) -> Result<SyncResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to sync database schema".to_string(),
));
}
#[cfg(feature = "cache")]
{
let cache_key = cache_key("database_metadata", database_id.0);
self.cache.invalidate(&cache_key);
}
self.service_manager
.database_service()
.ok_or_else(|| Error::Config("Database service not available".to_string()))?
.sync_database_schema(database_id)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn get_database_fields(&self, database_id: MetabaseId) -> Result<Vec<Field>> {
self.service_manager
.database_service()
.ok_or_else(|| Error::Config("Database service not available".to_string()))?
.get_database_fields(database_id)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn get_database_schemas(&self, database_id: MetabaseId) -> Result<Vec<String>> {
self.service_manager
.database_service()
.ok_or_else(|| Error::Config("Database service not available".to_string()))?
.get_database_schemas(database_id)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_dataset_query(&self, query: Value) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute dataset query".to_string(),
));
}
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.execute_raw_query(query)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_dataset_native(&self, query: Value) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute native dataset query".to_string(),
));
}
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.execute_raw_query(query)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_dataset_pivot(&self, query: Value) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute pivot dataset query".to_string(),
));
}
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.execute_pivot_query(query)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn export_dataset(&self, format: ExportFormat, query: Value) -> Result<Vec<u8>> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to export dataset".to_string(),
));
}
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.export_query(format.as_str(), query)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
#[cfg(feature = "query-builder")]
pub async fn execute_mbql_query(
&self,
database_id: MetabaseId,
query: MbqlQuery,
) -> Result<QueryResult> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to execute MBQL query".to_string(),
));
}
let dataset_query = query.to_dataset_query(database_id);
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.execute_dataset_query(dataset_query)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
#[cfg(feature = "query-builder")]
pub async fn export_mbql_query(
&self,
database_id: MetabaseId,
query: MbqlQuery,
format: ExportFormat,
) -> Result<Vec<u8>> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to export MBQL query".to_string(),
));
}
let dataset_query = query.to_dataset_query(database_id);
let query_value = serde_json::to_value(&dataset_query)
.map_err(|e| Error::Serialization(e.to_string()))?;
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.export_query(format.as_str(), query_value)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
pub async fn execute_sql(&self, database_id: MetabaseId, sql: &str) -> Result<QueryResult> {
let native_query = NativeQuery::new(sql);
self.execute_native_query(database_id, native_query).await
}
pub async fn execute_sql_with_params(
&self,
database_id: MetabaseId,
sql: &str,
params: HashMap<String, Value>,
) -> Result<QueryResult> {
let mut native_query = NativeQuery::new(sql);
for (name, value) in params {
native_query = native_query.with_param(&name, value);
}
self.execute_native_query(database_id, native_query).await
}
pub async fn export_sql_query(
&self,
database_id: MetabaseId,
sql: &str,
format: ExportFormat,
) -> Result<Vec<u8>> {
if !self.is_authenticated() {
return Err(Error::Authentication(
"Authentication required to export SQL query".to_string(),
));
}
let native_query = NativeQuery::new(sql);
let request = json!({
"database": database_id.0,
"type": "native",
"native": {
"query": native_query.query,
"template-tags": native_query.template_tags
}
});
self.service_manager
.query_service()
.ok_or_else(|| Error::Config("Query service not available".to_string()))?
.export_query(format.as_str(), request)
.await
.map_err(|e| Error::Config(format!("Service error: {}", e)))
}
}