rustio_core/admin/
schema_introspect.rs1use sqlx::Row;
24
25use crate::admin::admin_form_bridge::{AdminUiField, AdminUiModel};
26use crate::admin::admin_generator::{from_config, AdminModelConfig};
27use crate::error::Error;
28use crate::orm::Db;
29
30#[derive(Debug, Clone)]
34pub struct ColumnInfo {
35 pub name: String,
36 pub data_type: String,
37 pub not_null: bool,
38 pub is_primary_key: bool,
39}
40
41fn quote_ident(s: &str) -> String {
44 format!("\"{}\"", s.replace('"', "\"\""))
45}
46
47pub async fn get_table_columns(db: &Db, table: &str) -> Result<Vec<ColumnInfo>, Error> {
53 let sql = format!("PRAGMA table_info({})", quote_ident(table));
54 let rows = sqlx::query(&sql)
55 .fetch_all(db.pool())
56 .await
57 .map_err(Error::from)?;
58
59 if rows.is_empty() {
60 return Err(Error::NotFound);
61 }
62
63 let mut out = Vec::with_capacity(rows.len());
64 for row in rows {
65 let name: String = row.try_get("name").map_err(Error::from)?;
66 let data_type: String = row.try_get("type").map_err(Error::from)?;
67 let not_null: i64 = row.try_get("notnull").map_err(Error::from)?;
68 let pk: i64 = row.try_get("pk").map_err(Error::from)?;
69 out.push(ColumnInfo {
70 name,
71 data_type,
72 not_null: not_null != 0,
73 is_primary_key: pk != 0,
74 });
75 }
76 Ok(out)
77}
78
79pub fn column_to_field(col: &ColumnInfo) -> AdminUiField {
94 use crate::admin::admin_form_bridge::AdminDataType;
95
96 let name_static: &'static str = Box::leak(col.name.clone().into_boxed_str());
99 let label_static: &'static str = name_static;
101
102 let lname = col.name.to_lowercase();
103 let upper_type = col.data_type.to_uppercase();
104
105 let is_boolean_name = lname.starts_with("is_") || lname.ends_with("_flag");
106 let is_email = lname.contains("email");
107 let is_long_text = lname.contains("description") || lname.contains("content");
108
109 let dt = if is_boolean_name {
112 AdminDataType::Boolean
113 } else if is_email {
114 AdminDataType::Email
115 } else if upper_type.contains("INT") {
116 AdminDataType::Integer
117 } else if upper_type.contains("REAL")
118 || upper_type.contains("FLOA")
119 || upper_type.contains("DOUB")
120 || upper_type.contains("NUMERIC")
121 || upper_type.contains("DECIMAL")
122 {
123 AdminDataType::Float
124 } else if upper_type.contains("DATE") || upper_type.contains("TIME") {
125 AdminDataType::DateTime
126 } else if upper_type.contains("CHAR")
127 || upper_type.contains("TEXT")
128 || upper_type.contains("CLOB")
129 {
130 if is_long_text {
131 AdminDataType::Text
132 } else {
133 AdminDataType::String
134 }
135 } else {
136 AdminDataType::String
138 };
139
140 let mut field = match dt {
141 AdminDataType::Boolean => AdminUiField::boolean(name_static, label_static),
142 AdminDataType::Email => AdminUiField::email(name_static, label_static),
143 AdminDataType::Integer => AdminUiField::integer(name_static, label_static),
144 AdminDataType::Float => AdminUiField::float(name_static, label_static),
145 AdminDataType::DateTime => AdminUiField::datetime(name_static, label_static),
146 AdminDataType::Text => AdminUiField::textarea(name_static, label_static),
147 AdminDataType::String => AdminUiField::text(name_static, label_static),
148 };
149
150 field = field
151 .required(col.not_null)
152 .filterable(true)
153 .sortable(true)
154 .visible_in_table(true);
155 field
156}
157
158fn slug_from_table(table: &str) -> String {
162 table
163 .strip_prefix("admin_new_demo_")
164 .unwrap_or(table)
165 .to_string()
166}
167
168fn model_name_from_slug(slug: &str) -> String {
173 let mut base = slug.to_string();
174 if base.len() > 1 && base.ends_with('s') {
175 base.pop();
176 }
177 let mut chars = base.chars();
178 match chars.next() {
179 None => String::new(),
180 Some(c) => c.to_uppercase().chain(chars).collect(),
181 }
182}
183
184pub async fn generate_from_table(db: &Db, table: &str) -> Result<AdminModelConfig, Error> {
188 let columns = get_table_columns(db, table).await?;
189 if columns.is_empty() {
190 return Err(Error::NotFound);
191 }
192
193 let pk_name = columns
194 .iter()
195 .find(|c| c.is_primary_key)
196 .map(|c| c.name.clone())
197 .unwrap_or_else(|| "id".to_string());
198
199 let fields: Vec<AdminUiField> = columns
202 .iter()
203 .filter(|c| !c.is_primary_key)
204 .map(column_to_field)
205 .collect();
206
207 let searchable_fields: Vec<&'static str> = columns
210 .iter()
211 .filter(|c| !c.is_primary_key)
212 .filter(|c| {
213 let lname = c.name.to_lowercase();
214 let upper_type = c.data_type.to_uppercase();
215 let is_boolean_name = lname.starts_with("is_") || lname.ends_with("_flag");
216 !is_boolean_name
217 && (upper_type.contains("CHAR")
218 || upper_type.contains("TEXT")
219 || upper_type.contains("CLOB"))
220 })
221 .map(|c| {
222 let s: &'static str = Box::leak(c.name.clone().into_boxed_str());
223 s
224 })
225 .collect();
226
227 let primary_status_field: Option<&'static str> = columns
232 .iter()
233 .find(|c| c.name.to_lowercase().starts_with("is_"))
234 .map(|c| {
235 let s: &'static str = Box::leak(c.name.clone().into_boxed_str());
236 s
237 });
238
239 let slug_owned = slug_from_table(table);
240 let slug_static: &'static str = Box::leak(slug_owned.clone().into_boxed_str());
241 let model_name_static: &'static str =
242 Box::leak(model_name_from_slug(&slug_owned).into_boxed_str());
243 let table_static: &'static str = Box::leak(table.to_string().into_boxed_str());
244 let pk_static: &'static str = Box::leak(pk_name.into_boxed_str());
245
246 let mut cfg = AdminModelConfig::new(slug_static, model_name_static)
247 .table(table_static)
248 .primary_key(pk_static)
249 .fields(fields)
250 .searchable(searchable_fields);
251 if let Some(s) = primary_status_field {
252 cfg = cfg.status_field(s);
253 }
254 Ok(cfg)
257}
258
259pub async fn generate_model_from_table(
263 db: &Db,
264 table: &str,
265) -> Result<Box<dyn AdminUiModel>, Error> {
266 let cfg = generate_from_table(db, table).await?;
267 Ok(from_config(cfg))
268}