use sqlx::Row;
use crate::admin::admin_form_bridge::{AdminUiField, AdminUiModel};
use crate::admin::admin_generator::{from_config, AdminModelConfig};
use crate::error::Error;
use crate::orm::Db;
#[derive(Debug, Clone)]
pub struct ColumnInfo {
pub name: String,
pub data_type: String,
pub not_null: bool,
pub is_primary_key: bool,
}
fn quote_ident(s: &str) -> String {
format!("\"{}\"", s.replace('"', "\"\""))
}
pub async fn get_table_columns(db: &Db, table: &str) -> Result<Vec<ColumnInfo>, Error> {
let sql = format!("PRAGMA table_info({})", quote_ident(table));
let rows = sqlx::query(&sql)
.fetch_all(db.pool())
.await
.map_err(Error::from)?;
if rows.is_empty() {
return Err(Error::NotFound);
}
let mut out = Vec::with_capacity(rows.len());
for row in rows {
let name: String = row.try_get("name").map_err(Error::from)?;
let data_type: String = row.try_get("type").map_err(Error::from)?;
let not_null: i64 = row.try_get("notnull").map_err(Error::from)?;
let pk: i64 = row.try_get("pk").map_err(Error::from)?;
out.push(ColumnInfo {
name,
data_type,
not_null: not_null != 0,
is_primary_key: pk != 0,
});
}
Ok(out)
}
pub fn column_to_field(col: &ColumnInfo) -> AdminUiField {
use crate::admin::admin_form_bridge::AdminDataType;
let name_static: &'static str = Box::leak(col.name.clone().into_boxed_str());
let label_static: &'static str = name_static;
let lname = col.name.to_lowercase();
let upper_type = col.data_type.to_uppercase();
let is_boolean_name = lname.starts_with("is_") || lname.ends_with("_flag");
let is_email = lname.contains("email");
let is_long_text = lname.contains("description") || lname.contains("content");
let dt = if is_boolean_name {
AdminDataType::Boolean
} else if is_email {
AdminDataType::Email
} else if upper_type.contains("INT") {
AdminDataType::Integer
} else if upper_type.contains("REAL")
|| upper_type.contains("FLOA")
|| upper_type.contains("DOUB")
|| upper_type.contains("NUMERIC")
|| upper_type.contains("DECIMAL")
{
AdminDataType::Float
} else if upper_type.contains("DATE") || upper_type.contains("TIME") {
AdminDataType::DateTime
} else if upper_type.contains("CHAR")
|| upper_type.contains("TEXT")
|| upper_type.contains("CLOB")
{
if is_long_text {
AdminDataType::Text
} else {
AdminDataType::String
}
} else {
AdminDataType::String
};
let mut field = match dt {
AdminDataType::Boolean => AdminUiField::boolean(name_static, label_static),
AdminDataType::Email => AdminUiField::email(name_static, label_static),
AdminDataType::Integer => AdminUiField::integer(name_static, label_static),
AdminDataType::Float => AdminUiField::float(name_static, label_static),
AdminDataType::DateTime => AdminUiField::datetime(name_static, label_static),
AdminDataType::Text => AdminUiField::textarea(name_static, label_static),
AdminDataType::String => AdminUiField::text(name_static, label_static),
};
field = field
.required(col.not_null)
.filterable(true)
.sortable(true)
.visible_in_table(true);
field
}
fn slug_from_table(table: &str) -> String {
table
.strip_prefix("admin_new_demo_")
.unwrap_or(table)
.to_string()
}
fn model_name_from_slug(slug: &str) -> String {
let mut base = slug.to_string();
if base.len() > 1 && base.ends_with('s') {
base.pop();
}
let mut chars = base.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().chain(chars).collect(),
}
}
pub async fn generate_from_table(db: &Db, table: &str) -> Result<AdminModelConfig, Error> {
let columns = get_table_columns(db, table).await?;
if columns.is_empty() {
return Err(Error::NotFound);
}
let pk_name = columns
.iter()
.find(|c| c.is_primary_key)
.map(|c| c.name.clone())
.unwrap_or_else(|| "id".to_string());
let fields: Vec<AdminUiField> = columns
.iter()
.filter(|c| !c.is_primary_key)
.map(column_to_field)
.collect();
let searchable_fields: Vec<&'static str> = columns
.iter()
.filter(|c| !c.is_primary_key)
.filter(|c| {
let lname = c.name.to_lowercase();
let upper_type = c.data_type.to_uppercase();
let is_boolean_name = lname.starts_with("is_") || lname.ends_with("_flag");
!is_boolean_name
&& (upper_type.contains("CHAR")
|| upper_type.contains("TEXT")
|| upper_type.contains("CLOB"))
})
.map(|c| {
let s: &'static str = Box::leak(c.name.clone().into_boxed_str());
s
})
.collect();
let primary_status_field: Option<&'static str> = columns
.iter()
.find(|c| c.name.to_lowercase().starts_with("is_"))
.map(|c| {
let s: &'static str = Box::leak(c.name.clone().into_boxed_str());
s
});
let slug_owned = slug_from_table(table);
let slug_static: &'static str = Box::leak(slug_owned.clone().into_boxed_str());
let model_name_static: &'static str =
Box::leak(model_name_from_slug(&slug_owned).into_boxed_str());
let table_static: &'static str = Box::leak(table.to_string().into_boxed_str());
let pk_static: &'static str = Box::leak(pk_name.into_boxed_str());
let mut cfg = AdminModelConfig::new(slug_static, model_name_static)
.table(table_static)
.primary_key(pk_static)
.fields(fields)
.searchable(searchable_fields);
if let Some(s) = primary_status_field {
cfg = cfg.status_field(s);
}
Ok(cfg)
}
pub async fn generate_model_from_table(
db: &Db,
table: &str,
) -> Result<Box<dyn AdminUiModel>, Error> {
let cfg = generate_from_table(db, table).await?;
Ok(from_config(cfg))
}