use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::common::MetabaseId;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum ConnectionSource {
#[default]
Admin,
Setup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FieldId(pub i64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TableId(pub i64);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Database {
pub id: MetabaseId,
pub name: String,
pub engine: String,
pub details: Value,
#[serde(default)]
pub is_full_sync: bool,
#[serde(default)]
pub is_on_demand: bool,
#[serde(default)]
pub is_sample: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_field_values_schedule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata_sync_schedule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DatabaseTable {
pub id: TableId,
pub db_id: MetabaseId,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub display_name: String,
#[serde(default = "default_true")]
pub active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entity_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DatabaseField {
pub id: FieldId,
pub table_id: TableId,
pub name: String,
pub display_name: String,
pub database_type: String,
pub base_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub semantic_type: Option<String>,
#[serde(default = "default_true")]
pub active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub is_pk: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub fk_target_field_id: Option<FieldId>,
#[serde(default)]
pub position: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateDatabaseRequest {
pub name: String,
pub engine: String,
pub details: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_full_sync: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_on_demand: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_field_values_schedule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata_sync_schedule: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct UpdateDatabaseRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_full_sync: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_on_demand: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_field_values_schedule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata_sync_schedule: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DatabaseSyncStatus {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<DateTime<Utc>>,
}
fn default_true() -> bool {
true
}
impl Database {
pub fn builder(name: impl Into<String>, engine: impl Into<String>) -> DatabaseBuilder {
DatabaseBuilder::new(name, engine)
}
}
pub struct DatabaseBuilder {
name: String,
engine: String,
details: Value,
is_full_sync: bool,
is_on_demand: bool,
cache_field_values_schedule: Option<String>,
metadata_sync_schedule: Option<String>,
}
impl DatabaseBuilder {
pub fn new(name: impl Into<String>, engine: impl Into<String>) -> Self {
Self {
name: name.into(),
engine: engine.into(),
details: Value::Object(serde_json::Map::new()),
is_full_sync: true,
is_on_demand: false,
cache_field_values_schedule: None,
metadata_sync_schedule: None,
}
}
pub fn details(mut self, details: Value) -> Self {
self.details = details;
self
}
pub fn full_sync(mut self, enabled: bool) -> Self {
self.is_full_sync = enabled;
self
}
pub fn on_demand_sync(mut self, enabled: bool) -> Self {
self.is_on_demand = enabled;
self
}
pub fn cache_schedule(mut self, schedule: impl Into<String>) -> Self {
self.cache_field_values_schedule = Some(schedule.into());
self
}
pub fn sync_schedule(mut self, schedule: impl Into<String>) -> Self {
self.metadata_sync_schedule = Some(schedule.into());
self
}
pub fn build(self) -> Database {
Database {
id: MetabaseId(0), name: self.name,
engine: self.engine,
details: self.details,
is_full_sync: self.is_full_sync,
is_on_demand: self.is_on_demand,
is_sample: false,
cache_field_values_schedule: self.cache_field_values_schedule,
metadata_sync_schedule: self.metadata_sync_schedule,
created_at: None,
updated_at: None,
}
}
pub fn build_request(self) -> CreateDatabaseRequest {
CreateDatabaseRequest {
name: self.name,
engine: self.engine,
details: self.details,
is_full_sync: Some(self.is_full_sync),
is_on_demand: Some(self.is_on_demand),
cache_field_values_schedule: self.cache_field_values_schedule,
metadata_sync_schedule: self.metadata_sync_schedule,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_database_creation() {
let database = Database::builder("Test DB", "postgres")
.details(json!({
"host": "localhost",
"port": 5432,
"dbname": "testdb",
"user": "testuser"
}))
.full_sync(true)
.on_demand_sync(false)
.build();
assert_eq!(database.name, "Test DB");
assert_eq!(database.engine, "postgres");
assert!(database.is_full_sync);
assert!(!database.is_on_demand);
}
#[test]
fn test_database_table() {
let table = DatabaseTable {
id: TableId(1),
db_id: MetabaseId(1),
name: "users".to_string(),
schema: Some("public".to_string()),
display_name: "Users".to_string(),
active: true,
description: Some("User accounts".to_string()),
entity_type: Some("entity/UserTable".to_string()),
visibility_type: None,
created_at: None,
updated_at: None,
};
assert_eq!(table.name, "users");
assert_eq!(table.display_name, "Users");
assert!(table.active);
}
#[test]
fn test_database_field() {
let field = DatabaseField {
id: FieldId(1),
table_id: TableId(1),
name: "email".to_string(),
display_name: "Email".to_string(),
database_type: "VARCHAR(255)".to_string(),
base_type: "type/Text".to_string(),
semantic_type: Some("type/Email".to_string()),
active: true,
description: None,
is_pk: false,
fk_target_field_id: None,
position: 2,
visibility_type: None,
created_at: None,
updated_at: None,
};
assert_eq!(field.name, "email");
assert_eq!(field.base_type, "type/Text");
assert_eq!(field.semantic_type, Some("type/Email".to_string()));
assert!(!field.is_pk);
}
#[test]
fn test_create_database_request() {
let request = Database::builder("Production DB", "mysql")
.details(json!({
"host": "db.example.com",
"port": 3306,
"dbname": "production"
}))
.cache_schedule("0 0 * * *")
.build_request();
assert_eq!(request.name, "Production DB");
assert_eq!(request.engine, "mysql");
assert_eq!(request.is_full_sync, Some(true));
assert_eq!(
request.cache_field_values_schedule,
Some("0 0 * * *".to_string())
);
}
#[test]
fn test_update_database_request() {
let request = UpdateDatabaseRequest {
name: Some("Updated DB".to_string()),
is_full_sync: Some(false),
..Default::default()
};
assert_eq!(request.name, Some("Updated DB".to_string()));
assert_eq!(request.is_full_sync, Some(false));
assert!(request.details.is_none());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseMetadata {
pub id: MetabaseId,
pub name: String,
pub engine: String,
pub tables: Vec<TableMetadata>,
#[serde(default)]
pub features: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub native_permissions: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableMetadata {
pub id: TableId,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entity_type: Option<String>,
pub fields: Vec<FieldMetadata>,
#[serde(default = "default_true")]
pub active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldMetadata {
pub id: FieldId,
pub name: String,
pub display_name: String,
pub database_type: String,
pub base_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub semantic_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub is_pk: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub fk_target_field_id: Option<FieldId>,
pub position: i32,
#[serde(default = "default_true")]
pub active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncResult {
pub id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<DateTime<Utc>>,
}