use crate::forms::{PkKind, ValidationErrors};
pub fn pk_kind_for_table(table: &str) -> PkKind {
match crate::migrate::pk_meta_for_table(table).map(|(_, ty)| ty) {
Some(crate::orm::SqlType::Uuid) => PkKind::Uuid,
Some(crate::orm::SqlType::Text) => PkKind::Text,
_ => PkKind::BigInt,
}
}
pub fn validate_choice_member(
field: &str,
value: &str,
values: &[&'static str],
nullable: bool,
errs: &mut ValidationErrors,
) {
if value.is_empty() {
if !nullable {
errs.add(field, format!("{field} is required"));
}
return;
}
if !values.iter().any(|v| *v == value) {
errs.add(field, format!("{field} is not a valid choice"));
}
}
pub fn choice_options(values: &[&'static str], labels: &[&'static str]) -> Vec<(String, String)> {
values
.iter()
.zip(labels.iter())
.map(|(v, l)| ((*v).to_string(), (*l).to_string()))
.collect()
}
pub async fn validate_fk_exists(
field: &str,
id: &str,
target_table: &str,
nullable: bool,
errs: &mut ValidationErrors,
) {
if id.is_empty() {
if !nullable {
errs.add(field, format!("{field} is required"));
}
return;
}
let Some(meta) = crate::migrate::registered_models()
.into_iter()
.find(|m| m.table == target_table)
else {
return;
};
let Some(pk_col) = meta.pk_column().map(|c| c.name.clone()) else {
return;
};
let exists = crate::orm::dynamic::DynQuerySet::for_meta(&meta)
.filter_eq_string(&pk_col, id)
.count()
.await
.map(|n| n > 0)
.unwrap_or(false);
if !exists {
errs.add(field, format!("{field}: no matching record"));
}
}
pub async fn fetch_model_options(
target_table: &str,
label_field: Option<&str>,
) -> Vec<(String, String)> {
let Some(meta) = crate::migrate::registered_models()
.into_iter()
.find(|m| m.table == target_table)
else {
return Vec::new();
};
let Some(pk_col) = meta.pk_column().map(|c| c.name.clone()) else {
return Vec::new();
};
let label_col = label_field
.map(|s| s.to_string())
.or_else(|| {
meta.fields
.iter()
.find(|c| c.ty == crate::orm::SqlType::Text && c.name != pk_col)
.map(|c| c.name.clone())
})
.unwrap_or_else(|| pk_col.clone());
let rows = crate::orm::dynamic::DynQuerySet::for_meta(&meta)
.select_cols(&[pk_col.clone(), label_col.clone()])
.limit(1000)
.fetch_as_json()
.await
.unwrap_or_default();
rows.into_iter()
.filter_map(|obj| {
let id = json_scalar_to_string(obj.get(&pk_col)?);
let label = obj
.get(&label_col)
.map(json_scalar_to_string)
.unwrap_or_else(|| id.clone());
Some((id, label))
})
.collect()
}
fn json_scalar_to_string(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => String::new(),
other => other.to_string(),
}
}
fn pk_string_to_sea_value(id: &str, ty: crate::orm::SqlType) -> Option<sea_query::Value> {
match ty {
crate::orm::SqlType::SmallInt | crate::orm::SqlType::Integer => id
.parse::<i32>()
.ok()
.map(|n| sea_query::Value::Int(Some(n))),
crate::orm::SqlType::BigInt | crate::orm::SqlType::ForeignKey => id
.parse::<i64>()
.ok()
.map(|n| sea_query::Value::BigInt(Some(n))),
crate::orm::SqlType::Uuid | crate::orm::SqlType::Text => {
Some(sea_query::Value::String(Some(Box::new(id.to_string()))))
}
_ => Some(sea_query::Value::String(Some(Box::new(id.to_string())))),
}
}
pub fn parse_multi_ids(raw: &str) -> Vec<String> {
raw.split([',', ' ', '\n'])
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
pub async fn validate_multi_fk_exists(
field: &str,
ids: &[String],
target_table: &str,
errs: &mut ValidationErrors,
) -> Vec<sea_query::Value> {
if ids.is_empty() {
return Vec::new();
}
let Some(meta) = crate::migrate::registered_models()
.into_iter()
.find(|m| m.table == target_table)
else {
return Vec::new();
};
let Some(pk_col) = meta.pk_column().map(|c| c.name.clone()) else {
return Vec::new();
};
let rows = match crate::orm::dynamic::DynQuerySet::for_meta(&meta)
.select_cols(&[pk_col.clone()])
.filter_in_strings(&pk_col, ids)
.fetch_as_json()
.await
{
Ok(rows) => rows,
Err(e) => {
tracing::error!(
field = %field,
target_table = %target_table,
error = %e,
"M2M reference validation query failed; failing closed without flagging ids as missing",
);
errs.add_non_field(format!(
"could not validate {field} references (database error)"
));
return Vec::new();
}
};
let found: std::collections::HashSet<String> = rows
.into_iter()
.filter_map(|r| r.get(&pk_col).map(json_scalar_to_string))
.collect();
let pk_ty = meta
.fields
.iter()
.find(|c| c.name == pk_col)
.map(|c| c.ty)
.unwrap_or(crate::orm::SqlType::BigInt);
let mut out = Vec::with_capacity(ids.len());
for id in ids {
if found.contains(id) {
match pk_string_to_sea_value(id, pk_ty) {
Some(value) => out.push(value),
None => errs.add(
field,
format!("{field}: id {id} is not a valid primary key"),
),
}
} else {
errs.add(field, format!("{field}: id {id} has no matching record"));
}
}
out
}